ZhengyangZhang commited on
Commit
eb16f71
·
verified ·
1 Parent(s): 8086a45

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/matplotlib/_docstring.pyi +34 -0
  2. lib/python3.12/site-packages/matplotlib/_enums.py +177 -0
  3. lib/python3.12/site-packages/matplotlib/_type1font.py +879 -0
  4. lib/python3.12/site-packages/matplotlib/backend_managers.pyi +64 -0
  5. lib/python3.12/site-packages/matplotlib/category.py +235 -0
  6. lib/python3.12/site-packages/matplotlib/cbook.py +2415 -0
  7. lib/python3.12/site-packages/matplotlib/dviread.py +1139 -0
  8. lib/python3.12/site-packages/matplotlib/dviread.pyi +89 -0
  9. lib/python3.12/site-packages/matplotlib/inset.py +269 -0
  10. lib/python3.12/site-packages/matplotlib/layout_engine.py +309 -0
  11. lib/python3.12/site-packages/matplotlib/legend.py +1370 -0
  12. lib/python3.12/site-packages/matplotlib/lines.py +1720 -0
  13. lib/python3.12/site-packages/matplotlib/mathtext.py +140 -0
  14. lib/python3.12/site-packages/matplotlib/offsetbox.pyi +298 -0
  15. lib/python3.12/site-packages/matplotlib/pyplot.py +0 -0
  16. lib/python3.12/site-packages/matplotlib/sankey.pyi +61 -0
  17. lib/python3.12/site-packages/matplotlib/scale.py +767 -0
  18. lib/python3.12/site-packages/matplotlib/texmanager.py +368 -0
  19. lib/python3.12/site-packages/matplotlib/transforms.py +0 -0
  20. lib/python3.12/site-packages/sglang/srt/__pycache__/code_completion_parser.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/sglang/srt/__pycache__/custom_op.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/sglang/srt/__pycache__/model_parallel.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/sglang/srt/__pycache__/patch_torch.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/sglang/srt/__pycache__/reasoning_parser.cpython-312.pyc +0 -0
  25. lib/python3.12/site-packages/sglang/srt/__pycache__/utils.cpython-312.pyc +0 -0
  26. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/activation.cpython-312.pyc +0 -0
  27. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/dp_attention.cpython-312.pyc +0 -0
  28. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/elementwise.cpython-312.pyc +0 -0
  29. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/layernorm.cpython-312.pyc +0 -0
  30. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/linear.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/logits_processor.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/parameter.cpython-312.pyc +0 -0
  33. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/pooler.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/radix_attention.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/rotary_embedding.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/sampler.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/torchao_utils.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/sglang/srt/layers/__pycache__/vocab_parallel_embedding.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/sglang/srt/layers/attention/base_attn_backend.py +111 -0
  40. lib/python3.12/site-packages/sglang/srt/layers/attention/cutlass_mla_backend.py +278 -0
  41. lib/python3.12/site-packages/sglang/srt/layers/attention/double_sparsity_backend.py +257 -0
  42. lib/python3.12/site-packages/sglang/srt/layers/attention/flashattention_backend.py +1851 -0
  43. lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_backend.py +1266 -0
  44. lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_mla_backend.py +840 -0
  45. lib/python3.12/site-packages/sglang/srt/layers/attention/flashmla_backend.py +281 -0
  46. lib/python3.12/site-packages/sglang/srt/layers/attention/torch_native_backend.py +267 -0
  47. lib/python3.12/site-packages/sglang/srt/layers/attention/triton_backend.py +733 -0
  48. lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/decode_attention.py +733 -0
  49. lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/double_sparsity_attention.py +1106 -0
  50. lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/extend_attention.py +467 -0
lib/python3.12/site-packages/matplotlib/_docstring.pyi ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Callable
2
+ from typing import Any, TypeVar, overload
3
+
4
+
5
+ _T = TypeVar('_T')
6
+
7
+
8
+ def kwarg_doc(text: str) -> Callable[[_T], _T]: ...
9
+
10
+
11
+ class Substitution:
12
+ @overload
13
+ def __init__(self, *args: str): ...
14
+ @overload
15
+ def __init__(self, **kwargs: str): ...
16
+ def __call__(self, func: _T) -> _T: ...
17
+ def update(self, *args, **kwargs): ... # type: ignore[no-untyped-def]
18
+
19
+
20
+ class _ArtistKwdocLoader(dict[str, str]):
21
+ def __missing__(self, key: str) -> str: ...
22
+
23
+
24
+ class _ArtistPropertiesSubstitution:
25
+ def __init__(self) -> None: ...
26
+ def register(self, **kwargs) -> None: ...
27
+ def __call__(self, obj: _T) -> _T: ...
28
+
29
+
30
+ def copy(source: Any) -> Callable[[_T], _T]: ...
31
+
32
+
33
+ dedent_interpd: _ArtistPropertiesSubstitution
34
+ interpd: _ArtistPropertiesSubstitution
lib/python3.12/site-packages/matplotlib/_enums.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Enums representing sets of strings that Matplotlib uses as input parameters.
3
+
4
+ Matplotlib often uses simple data types like strings or tuples to define a
5
+ concept; e.g. the line capstyle can be specified as one of 'butt', 'round',
6
+ or 'projecting'. The classes in this module are used internally and serve to
7
+ document these concepts formally.
8
+
9
+ As an end-user you will not use these classes directly, but only the values
10
+ they define.
11
+ """
12
+
13
+ from enum import Enum
14
+ from matplotlib import _docstring
15
+
16
+
17
+ class JoinStyle(str, Enum):
18
+ """
19
+ Define how the connection between two line segments is drawn.
20
+
21
+ For a visual impression of each *JoinStyle*, `view these docs online
22
+ <JoinStyle>`, or run `JoinStyle.demo`.
23
+
24
+ Lines in Matplotlib are typically defined by a 1D `~.path.Path` and a
25
+ finite ``linewidth``, where the underlying 1D `~.path.Path` represents the
26
+ center of the stroked line.
27
+
28
+ By default, `~.backend_bases.GraphicsContextBase` defines the boundaries of
29
+ a stroked line to simply be every point within some radius,
30
+ ``linewidth/2``, away from any point of the center line. However, this
31
+ results in corners appearing "rounded", which may not be the desired
32
+ behavior if you are drawing, for example, a polygon or pointed star.
33
+
34
+ **Supported values:**
35
+
36
+ .. rst-class:: value-list
37
+
38
+ 'miter'
39
+ the "arrow-tip" style. Each boundary of the filled-in area will
40
+ extend in a straight line parallel to the tangent vector of the
41
+ centerline at the point it meets the corner, until they meet in a
42
+ sharp point.
43
+ 'round'
44
+ stokes every point within a radius of ``linewidth/2`` of the center
45
+ lines.
46
+ 'bevel'
47
+ the "squared-off" style. It can be thought of as a rounded corner
48
+ where the "circular" part of the corner has been cut off.
49
+
50
+ .. note::
51
+
52
+ Very long miter tips are cut off (to form a *bevel*) after a
53
+ backend-dependent limit called the "miter limit", which specifies the
54
+ maximum allowed ratio of miter length to line width. For example, the
55
+ PDF backend uses the default value of 10 specified by the PDF standard,
56
+ while the SVG backend does not even specify the miter limit, resulting
57
+ in a default value of 4 per the SVG specification. Matplotlib does not
58
+ currently allow the user to adjust this parameter.
59
+
60
+ A more detailed description of the effect of a miter limit can be found
61
+ in the `Mozilla Developer Docs
62
+ <https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit>`_
63
+
64
+ .. plot::
65
+ :alt: Demo of possible JoinStyle's
66
+
67
+ from matplotlib._enums import JoinStyle
68
+ JoinStyle.demo()
69
+
70
+ """
71
+
72
+ miter = "miter"
73
+ round = "round"
74
+ bevel = "bevel"
75
+
76
+ @staticmethod
77
+ def demo():
78
+ """Demonstrate how each JoinStyle looks for various join angles."""
79
+ import numpy as np
80
+ import matplotlib.pyplot as plt
81
+
82
+ def plot_angle(ax, x, y, angle, style):
83
+ phi = np.radians(angle)
84
+ xx = [x + .5, x, x + .5*np.cos(phi)]
85
+ yy = [y, y, y + .5*np.sin(phi)]
86
+ ax.plot(xx, yy, lw=12, color='tab:blue', solid_joinstyle=style)
87
+ ax.plot(xx, yy, lw=1, color='black')
88
+ ax.plot(xx[1], yy[1], 'o', color='tab:red', markersize=3)
89
+
90
+ fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
91
+ ax.set_title('Join style')
92
+ for x, style in enumerate(['miter', 'round', 'bevel']):
93
+ ax.text(x, 5, style)
94
+ for y, angle in enumerate([20, 45, 60, 90, 120]):
95
+ plot_angle(ax, x, y, angle, style)
96
+ if x == 0:
97
+ ax.text(-1.3, y, f'{angle} degrees')
98
+ ax.set_xlim(-1.5, 2.75)
99
+ ax.set_ylim(-.5, 5.5)
100
+ ax.set_axis_off()
101
+ fig.show()
102
+
103
+
104
+ JoinStyle.input_description = "{" \
105
+ + ", ".join([f"'{js.name}'" for js in JoinStyle]) \
106
+ + "}"
107
+
108
+
109
+ class CapStyle(str, Enum):
110
+ r"""
111
+ Define how the two endpoints (caps) of an unclosed line are drawn.
112
+
113
+ How to draw the start and end points of lines that represent a closed curve
114
+ (i.e. that end in a `~.path.Path.CLOSEPOLY`) is controlled by the line's
115
+ `JoinStyle`. For all other lines, how the start and end points are drawn is
116
+ controlled by the *CapStyle*.
117
+
118
+ For a visual impression of each *CapStyle*, `view these docs online
119
+ <CapStyle>` or run `CapStyle.demo`.
120
+
121
+ By default, `~.backend_bases.GraphicsContextBase` draws a stroked line as
122
+ squared off at its endpoints.
123
+
124
+ **Supported values:**
125
+
126
+ .. rst-class:: value-list
127
+
128
+ 'butt'
129
+ the line is squared off at its endpoint.
130
+ 'projecting'
131
+ the line is squared off as in *butt*, but the filled in area
132
+ extends beyond the endpoint a distance of ``linewidth/2``.
133
+ 'round'
134
+ like *butt*, but a semicircular cap is added to the end of the
135
+ line, of radius ``linewidth/2``.
136
+
137
+ .. plot::
138
+ :alt: Demo of possible CapStyle's
139
+
140
+ from matplotlib._enums import CapStyle
141
+ CapStyle.demo()
142
+
143
+ """
144
+ butt = "butt"
145
+ projecting = "projecting"
146
+ round = "round"
147
+
148
+ @staticmethod
149
+ def demo():
150
+ """Demonstrate how each CapStyle looks for a thick line segment."""
151
+ import matplotlib.pyplot as plt
152
+
153
+ fig = plt.figure(figsize=(4, 1.2))
154
+ ax = fig.add_axes([0, 0, 1, 0.8])
155
+ ax.set_title('Cap style')
156
+
157
+ for x, style in enumerate(['butt', 'round', 'projecting']):
158
+ ax.text(x+0.25, 0.85, style, ha='center')
159
+ xx = [x, x+0.5]
160
+ yy = [0, 0]
161
+ ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style)
162
+ ax.plot(xx, yy, lw=1, color='black')
163
+ ax.plot(xx, yy, 'o', color='tab:red', markersize=3)
164
+
165
+ ax.set_ylim(-.5, 1.5)
166
+ ax.set_axis_off()
167
+ fig.show()
168
+
169
+
170
+ CapStyle.input_description = "{" \
171
+ + ", ".join([f"'{cs.name}'" for cs in CapStyle]) \
172
+ + "}"
173
+
174
+ _docstring.interpd.register(
175
+ JoinStyle=JoinStyle.input_description,
176
+ CapStyle=CapStyle.input_description,
177
+ )
lib/python3.12/site-packages/matplotlib/_type1font.py ADDED
@@ -0,0 +1,879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A class representing a Type 1 font.
3
+
4
+ This version reads pfa and pfb files and splits them for embedding in
5
+ pdf files. It also supports SlantFont and ExtendFont transformations,
6
+ similarly to pdfTeX and friends. There is no support yet for subsetting.
7
+
8
+ Usage::
9
+
10
+ font = Type1Font(filename)
11
+ clear_part, encrypted_part, finale = font.parts
12
+ slanted_font = font.transform({'slant': 0.167})
13
+ extended_font = font.transform({'extend': 1.2})
14
+
15
+ Sources:
16
+
17
+ * Adobe Technical Note #5040, Supporting Downloadable PostScript
18
+ Language Fonts.
19
+
20
+ * Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing,
21
+ v1.1, 1993. ISBN 0-201-57044-0.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import binascii
27
+ import functools
28
+ import logging
29
+ import re
30
+ import string
31
+ import struct
32
+ import typing as T
33
+
34
+ import numpy as np
35
+
36
+ from matplotlib.cbook import _format_approx
37
+ from . import _api
38
+
39
+ _log = logging.getLogger(__name__)
40
+
41
+
42
+ class _Token:
43
+ """
44
+ A token in a PostScript stream.
45
+
46
+ Attributes
47
+ ----------
48
+ pos : int
49
+ Position, i.e. offset from the beginning of the data.
50
+ raw : str
51
+ Raw text of the token.
52
+ kind : str
53
+ Description of the token (for debugging or testing).
54
+ """
55
+ __slots__ = ('pos', 'raw')
56
+ kind = '?'
57
+
58
+ def __init__(self, pos, raw):
59
+ _log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw)
60
+ self.pos = pos
61
+ self.raw = raw
62
+
63
+ def __str__(self):
64
+ return f"<{self.kind} {self.raw} @{self.pos}>"
65
+
66
+ def endpos(self):
67
+ """Position one past the end of the token"""
68
+ return self.pos + len(self.raw)
69
+
70
+ def is_keyword(self, *names):
71
+ """Is this a name token with one of the names?"""
72
+ return False
73
+
74
+ def is_slash_name(self):
75
+ """Is this a name token that starts with a slash?"""
76
+ return False
77
+
78
+ def is_delim(self):
79
+ """Is this a delimiter token?"""
80
+ return False
81
+
82
+ def is_number(self):
83
+ """Is this a number token?"""
84
+ return False
85
+
86
+ def value(self):
87
+ return self.raw
88
+
89
+
90
+ class _NameToken(_Token):
91
+ kind = 'name'
92
+
93
+ def is_slash_name(self):
94
+ return self.raw.startswith('/')
95
+
96
+ def value(self):
97
+ return self.raw[1:]
98
+
99
+
100
+ class _BooleanToken(_Token):
101
+ kind = 'boolean'
102
+
103
+ def value(self):
104
+ return self.raw == 'true'
105
+
106
+
107
+ class _KeywordToken(_Token):
108
+ kind = 'keyword'
109
+
110
+ def is_keyword(self, *names):
111
+ return self.raw in names
112
+
113
+
114
+ class _DelimiterToken(_Token):
115
+ kind = 'delimiter'
116
+
117
+ def is_delim(self):
118
+ return True
119
+
120
+ def opposite(self):
121
+ return {'[': ']', ']': '[',
122
+ '{': '}', '}': '{',
123
+ '<<': '>>', '>>': '<<'
124
+ }[self.raw]
125
+
126
+
127
+ class _WhitespaceToken(_Token):
128
+ kind = 'whitespace'
129
+
130
+
131
+ class _StringToken(_Token):
132
+ kind = 'string'
133
+ _escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})')
134
+ _replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n',
135
+ 'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'}
136
+ _ws_re = re.compile('[\0\t\r\f\n ]')
137
+
138
+ @classmethod
139
+ def _escape(cls, match):
140
+ group = match.group(1)
141
+ try:
142
+ return cls._replacements[group]
143
+ except KeyError:
144
+ return chr(int(group, 8))
145
+
146
+ @functools.lru_cache
147
+ def value(self):
148
+ if self.raw[0] == '(':
149
+ return self._escapes_re.sub(self._escape, self.raw[1:-1])
150
+ else:
151
+ data = self._ws_re.sub('', self.raw[1:-1])
152
+ if len(data) % 2 == 1:
153
+ data += '0'
154
+ return binascii.unhexlify(data)
155
+
156
+
157
+ class _BinaryToken(_Token):
158
+ kind = 'binary'
159
+
160
+ def value(self):
161
+ return self.raw[1:]
162
+
163
+
164
+ class _NumberToken(_Token):
165
+ kind = 'number'
166
+
167
+ def is_number(self):
168
+ return True
169
+
170
+ def value(self):
171
+ if '.' not in self.raw:
172
+ return int(self.raw)
173
+ else:
174
+ return float(self.raw)
175
+
176
+
177
+ def _tokenize(data: bytes, skip_ws: bool) -> T.Generator[_Token, int, None]:
178
+ """
179
+ A generator that produces _Token instances from Type-1 font code.
180
+
181
+ The consumer of the generator may send an integer to the tokenizer to
182
+ indicate that the next token should be _BinaryToken of the given length.
183
+
184
+ Parameters
185
+ ----------
186
+ data : bytes
187
+ The data of the font to tokenize.
188
+
189
+ skip_ws : bool
190
+ If true, the generator will drop any _WhitespaceTokens from the output.
191
+ """
192
+
193
+ text = data.decode('ascii', 'replace')
194
+ whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*')
195
+ token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+')
196
+ instring_re = re.compile(r'[()\\]')
197
+ hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$')
198
+ oct_re = re.compile(r'[0-7]{1,3}')
199
+ pos = 0
200
+ next_binary: int | None = None
201
+
202
+ while pos < len(text):
203
+ if next_binary is not None:
204
+ n = next_binary
205
+ next_binary = (yield _BinaryToken(pos, data[pos:pos+n]))
206
+ pos += n
207
+ continue
208
+ match = whitespace_or_comment_re.match(text, pos)
209
+ if match:
210
+ if not skip_ws:
211
+ next_binary = (yield _WhitespaceToken(pos, match.group()))
212
+ pos = match.end()
213
+ elif text[pos] == '(':
214
+ # PostScript string rules:
215
+ # - parentheses must be balanced
216
+ # - backslashes escape backslashes and parens
217
+ # - also codes \n\r\t\b\f and octal escapes are recognized
218
+ # - other backslashes do not escape anything
219
+ start = pos
220
+ pos += 1
221
+ depth = 1
222
+ while depth:
223
+ match = instring_re.search(text, pos)
224
+ if match is None:
225
+ raise ValueError(
226
+ f'Unterminated string starting at {start}')
227
+ pos = match.end()
228
+ if match.group() == '(':
229
+ depth += 1
230
+ elif match.group() == ')':
231
+ depth -= 1
232
+ else: # a backslash
233
+ char = text[pos]
234
+ if char in r'\()nrtbf':
235
+ pos += 1
236
+ else:
237
+ octal = oct_re.match(text, pos)
238
+ if octal:
239
+ pos = octal.end()
240
+ else:
241
+ pass # non-escaping backslash
242
+ next_binary = (yield _StringToken(start, text[start:pos]))
243
+ elif text[pos:pos + 2] in ('<<', '>>'):
244
+ next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2]))
245
+ pos += 2
246
+ elif text[pos] == '<':
247
+ start = pos
248
+ try:
249
+ pos = text.index('>', pos) + 1
250
+ except ValueError as e:
251
+ raise ValueError(f'Unterminated hex string starting at {start}'
252
+ ) from e
253
+ if not hex_re.match(text[start:pos]):
254
+ raise ValueError(f'Malformed hex string starting at {start}')
255
+ next_binary = (yield _StringToken(pos, text[start:pos]))
256
+ else:
257
+ match = token_re.match(text, pos)
258
+ if match:
259
+ raw = match.group()
260
+ if raw.startswith('/'):
261
+ next_binary = (yield _NameToken(pos, raw))
262
+ elif match.group() in ('true', 'false'):
263
+ next_binary = (yield _BooleanToken(pos, raw))
264
+ else:
265
+ try:
266
+ float(raw)
267
+ next_binary = (yield _NumberToken(pos, raw))
268
+ except ValueError:
269
+ next_binary = (yield _KeywordToken(pos, raw))
270
+ pos = match.end()
271
+ else:
272
+ next_binary = (yield _DelimiterToken(pos, text[pos]))
273
+ pos += 1
274
+
275
+
276
+ class _BalancedExpression(_Token):
277
+ pass
278
+
279
+
280
+ def _expression(initial, tokens, data):
281
+ """
282
+ Consume some number of tokens and return a balanced PostScript expression.
283
+
284
+ Parameters
285
+ ----------
286
+ initial : _Token
287
+ The token that triggered parsing a balanced expression.
288
+ tokens : iterator of _Token
289
+ Following tokens.
290
+ data : bytes
291
+ Underlying data that the token positions point to.
292
+
293
+ Returns
294
+ -------
295
+ _BalancedExpression
296
+ """
297
+ delim_stack = []
298
+ token = initial
299
+ while True:
300
+ if token.is_delim():
301
+ if token.raw in ('[', '{'):
302
+ delim_stack.append(token)
303
+ elif token.raw in (']', '}'):
304
+ if not delim_stack:
305
+ raise RuntimeError(f"unmatched closing token {token}")
306
+ match = delim_stack.pop()
307
+ if match.raw != token.opposite():
308
+ raise RuntimeError(
309
+ f"opening token {match} closed by {token}"
310
+ )
311
+ if not delim_stack:
312
+ break
313
+ else:
314
+ raise RuntimeError(f'unknown delimiter {token}')
315
+ elif not delim_stack:
316
+ break
317
+ token = next(tokens)
318
+ return _BalancedExpression(
319
+ initial.pos,
320
+ data[initial.pos:token.endpos()].decode('ascii', 'replace')
321
+ )
322
+
323
+
324
+ class Type1Font:
325
+ """
326
+ A class representing a Type-1 font, for use by backends.
327
+
328
+ Attributes
329
+ ----------
330
+ parts : tuple
331
+ A 3-tuple of the cleartext part, the encrypted part, and the finale of
332
+ zeros.
333
+
334
+ decrypted : bytes
335
+ The decrypted form of ``parts[1]``.
336
+
337
+ prop : dict[str, Any]
338
+ A dictionary of font properties. Noteworthy keys include:
339
+
340
+ - FontName: PostScript name of the font
341
+ - Encoding: dict from numeric codes to glyph names
342
+ - FontMatrix: bytes object encoding a matrix
343
+ - UniqueID: optional font identifier, dropped when modifying the font
344
+ - CharStrings: dict from glyph names to byte code
345
+ - Subrs: array of byte code subroutines
346
+ - OtherSubrs: bytes object encoding some PostScript code
347
+ """
348
+ __slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr')
349
+ # the _pos dict contains (begin, end) indices to parts[0] + decrypted
350
+ # so that they can be replaced when transforming the font;
351
+ # but since sometimes a definition appears in both parts[0] and decrypted,
352
+ # _pos[name] is an array of such pairs
353
+ #
354
+ # _abbr maps three standard abbreviations to their particular names in
355
+ # this font (e.g. 'RD' is named '-|' in some fonts)
356
+
357
+ def __init__(self, input):
358
+ """
359
+ Initialize a Type-1 font.
360
+
361
+ Parameters
362
+ ----------
363
+ input : str or 3-tuple
364
+ Either a pfb file name, or a 3-tuple of already-decoded Type-1
365
+ font `~.Type1Font.parts`.
366
+ """
367
+ if isinstance(input, tuple) and len(input) == 3:
368
+ self.parts = input
369
+ else:
370
+ with open(input, 'rb') as file:
371
+ data = self._read(file)
372
+ self.parts = self._split(data)
373
+
374
+ self.decrypted = self._decrypt(self.parts[1], 'eexec')
375
+ self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'}
376
+ self._parse()
377
+
378
+ def _read(self, file):
379
+ """Read the font from a file, decoding into usable parts."""
380
+ rawdata = file.read()
381
+ if not rawdata.startswith(b'\x80'):
382
+ return rawdata
383
+
384
+ data = b''
385
+ while rawdata:
386
+ if not rawdata.startswith(b'\x80'):
387
+ raise RuntimeError('Broken pfb file (expected byte 128, '
388
+ 'got %d)' % rawdata[0])
389
+ type = rawdata[1]
390
+ if type in (1, 2):
391
+ length, = struct.unpack('<i', rawdata[2:6])
392
+ segment = rawdata[6:6 + length]
393
+ rawdata = rawdata[6 + length:]
394
+
395
+ if type == 1: # ASCII text: include verbatim
396
+ data += segment
397
+ elif type == 2: # binary data: encode in hexadecimal
398
+ data += binascii.hexlify(segment)
399
+ elif type == 3: # end of file
400
+ break
401
+ else:
402
+ raise RuntimeError('Unknown segment type %d in pfb file' % type)
403
+
404
+ return data
405
+
406
+ def _split(self, data):
407
+ """
408
+ Split the Type 1 font into its three main parts.
409
+
410
+ The three parts are: (1) the cleartext part, which ends in a
411
+ eexec operator; (2) the encrypted part; (3) the fixed part,
412
+ which contains 512 ASCII zeros possibly divided on various
413
+ lines, a cleartomark operator, and possibly something else.
414
+ """
415
+
416
+ # Cleartext part: just find the eexec and skip whitespace
417
+ idx = data.index(b'eexec')
418
+ idx += len(b'eexec')
419
+ while data[idx] in b' \t\r\n':
420
+ idx += 1
421
+ len1 = idx
422
+
423
+ # Encrypted part: find the cleartomark operator and count
424
+ # zeros backward
425
+ idx = data.rindex(b'cleartomark') - 1
426
+ zeros = 512
427
+ while zeros and data[idx] in b'0' or data[idx] in b'\r\n':
428
+ if data[idx] in b'0':
429
+ zeros -= 1
430
+ idx -= 1
431
+ if zeros:
432
+ # this may have been a problem on old implementations that
433
+ # used the zeros as necessary padding
434
+ _log.info('Insufficiently many zeros in Type 1 font')
435
+
436
+ # Convert encrypted part to binary (if we read a pfb file, we may end
437
+ # up converting binary to hexadecimal to binary again; but if we read
438
+ # a pfa file, this part is already in hex, and I am not quite sure if
439
+ # even the pfb format guarantees that it will be in binary).
440
+ idx1 = len1 + ((idx - len1 + 2) & ~1) # ensure an even number of bytes
441
+ binary = binascii.unhexlify(data[len1:idx1])
442
+
443
+ return data[:len1], binary, data[idx+1:]
444
+
445
+ @staticmethod
446
+ def _decrypt(ciphertext, key, ndiscard=4):
447
+ """
448
+ Decrypt ciphertext using the Type-1 font algorithm.
449
+
450
+ The algorithm is described in Adobe's "Adobe Type 1 Font Format".
451
+ The key argument can be an integer, or one of the strings
452
+ 'eexec' and 'charstring', which map to the key specified for the
453
+ corresponding part of Type-1 fonts.
454
+
455
+ The ndiscard argument should be an integer, usually 4.
456
+ That number of bytes is discarded from the beginning of plaintext.
457
+ """
458
+
459
+ key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
460
+ plaintext = []
461
+ for byte in ciphertext:
462
+ plaintext.append(byte ^ (key >> 8))
463
+ key = ((key+byte) * 52845 + 22719) & 0xffff
464
+
465
+ return bytes(plaintext[ndiscard:])
466
+
467
+ @staticmethod
468
+ def _encrypt(plaintext, key, ndiscard=4):
469
+ """
470
+ Encrypt plaintext using the Type-1 font algorithm.
471
+
472
+ The algorithm is described in Adobe's "Adobe Type 1 Font Format".
473
+ The key argument can be an integer, or one of the strings
474
+ 'eexec' and 'charstring', which map to the key specified for the
475
+ corresponding part of Type-1 fonts.
476
+
477
+ The ndiscard argument should be an integer, usually 4. That
478
+ number of bytes is prepended to the plaintext before encryption.
479
+ This function prepends NUL bytes for reproducibility, even though
480
+ the original algorithm uses random bytes, presumably to avoid
481
+ cryptanalysis.
482
+ """
483
+
484
+ key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key)
485
+ ciphertext = []
486
+ for byte in b'\0' * ndiscard + plaintext:
487
+ c = byte ^ (key >> 8)
488
+ ciphertext.append(c)
489
+ key = ((key + c) * 52845 + 22719) & 0xffff
490
+
491
+ return bytes(ciphertext)
492
+
493
+ def _parse(self):
494
+ """
495
+ Find the values of various font properties. This limited kind
496
+ of parsing is described in Chapter 10 "Adobe Type Manager
497
+ Compatibility" of the Type-1 spec.
498
+ """
499
+ # Start with reasonable defaults
500
+ prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False,
501
+ 'UnderlinePosition': -100, 'UnderlineThickness': 50}
502
+ pos = {}
503
+ data = self.parts[0] + self.decrypted
504
+
505
+ source = _tokenize(data, True)
506
+ while True:
507
+ # See if there is a key to be assigned a value
508
+ # e.g. /FontName in /FontName /Helvetica def
509
+ try:
510
+ token = next(source)
511
+ except StopIteration:
512
+ break
513
+ if token.is_delim():
514
+ # skip over this - we want top-level keys only
515
+ _expression(token, source, data)
516
+ if token.is_slash_name():
517
+ key = token.value()
518
+ keypos = token.pos
519
+ else:
520
+ continue
521
+
522
+ # Some values need special parsing
523
+ if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'):
524
+ prop[key], endpos = {
525
+ 'Subrs': self._parse_subrs,
526
+ 'CharStrings': self._parse_charstrings,
527
+ 'Encoding': self._parse_encoding,
528
+ 'OtherSubrs': self._parse_othersubrs
529
+ }[key](source, data)
530
+ pos.setdefault(key, []).append((keypos, endpos))
531
+ continue
532
+
533
+ try:
534
+ token = next(source)
535
+ except StopIteration:
536
+ break
537
+
538
+ if isinstance(token, _KeywordToken):
539
+ # constructs like
540
+ # FontDirectory /Helvetica known {...} {...} ifelse
541
+ # mean the key was not really a key
542
+ continue
543
+
544
+ if token.is_delim():
545
+ value = _expression(token, source, data).raw
546
+ else:
547
+ value = token.value()
548
+
549
+ # look for a 'def' possibly preceded by access modifiers
550
+ try:
551
+ kw = next(
552
+ kw for kw in source
553
+ if not kw.is_keyword('readonly', 'noaccess', 'executeonly')
554
+ )
555
+ except StopIteration:
556
+ break
557
+
558
+ # sometimes noaccess def and readonly def are abbreviated
559
+ if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']):
560
+ prop[key] = value
561
+ pos.setdefault(key, []).append((keypos, kw.endpos()))
562
+
563
+ # detect the standard abbreviations
564
+ if value == '{noaccess def}':
565
+ self._abbr['ND'] = key
566
+ elif value == '{noaccess put}':
567
+ self._abbr['NP'] = key
568
+ elif value == '{string currentfile exch readstring pop}':
569
+ self._abbr['RD'] = key
570
+
571
+ # Fill in the various *Name properties
572
+ if 'FontName' not in prop:
573
+ prop['FontName'] = (prop.get('FullName') or
574
+ prop.get('FamilyName') or
575
+ 'Unknown')
576
+ if 'FullName' not in prop:
577
+ prop['FullName'] = prop['FontName']
578
+ if 'FamilyName' not in prop:
579
+ extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|'
580
+ '(ultra)?light|extra|condensed))+$')
581
+ prop['FamilyName'] = re.sub(extras, '', prop['FullName'])
582
+ # Decrypt the encrypted parts
583
+ ndiscard = prop.get('lenIV', 4)
584
+ cs = prop['CharStrings']
585
+ for key, value in cs.items():
586
+ cs[key] = self._decrypt(value, 'charstring', ndiscard)
587
+ if 'Subrs' in prop:
588
+ prop['Subrs'] = [
589
+ self._decrypt(value, 'charstring', ndiscard)
590
+ for value in prop['Subrs']
591
+ ]
592
+
593
+ self.prop = prop
594
+ self._pos = pos
595
+
596
+ def _parse_subrs(self, tokens, _data):
597
+ count_token = next(tokens)
598
+ if not count_token.is_number():
599
+ raise RuntimeError(
600
+ f"Token following /Subrs must be a number, was {count_token}"
601
+ )
602
+ count = count_token.value()
603
+ array = [None] * count
604
+ next(t for t in tokens if t.is_keyword('array'))
605
+ for _ in range(count):
606
+ next(t for t in tokens if t.is_keyword('dup'))
607
+ index_token = next(tokens)
608
+ if not index_token.is_number():
609
+ raise RuntimeError(
610
+ "Token following dup in Subrs definition must be a "
611
+ f"number, was {index_token}"
612
+ )
613
+ nbytes_token = next(tokens)
614
+ if not nbytes_token.is_number():
615
+ raise RuntimeError(
616
+ "Second token following dup in Subrs definition must "
617
+ f"be a number, was {nbytes_token}"
618
+ )
619
+ token = next(tokens)
620
+ if not token.is_keyword(self._abbr['RD']):
621
+ raise RuntimeError(
622
+ f"Token preceding subr must be {self._abbr['RD']}, "
623
+ f"was {token}"
624
+ )
625
+ binary_token = tokens.send(1+nbytes_token.value())
626
+ array[index_token.value()] = binary_token.value()
627
+
628
+ return array, next(tokens).endpos()
629
+
630
+ @staticmethod
631
+ def _parse_charstrings(tokens, _data):
632
+ count_token = next(tokens)
633
+ if not count_token.is_number():
634
+ raise RuntimeError(
635
+ "Token following /CharStrings must be a number, "
636
+ f"was {count_token}"
637
+ )
638
+ count = count_token.value()
639
+ charstrings = {}
640
+ next(t for t in tokens if t.is_keyword('begin'))
641
+ while True:
642
+ token = next(t for t in tokens
643
+ if t.is_keyword('end') or t.is_slash_name())
644
+ if token.raw == 'end':
645
+ return charstrings, token.endpos()
646
+ glyphname = token.value()
647
+ nbytes_token = next(tokens)
648
+ if not nbytes_token.is_number():
649
+ raise RuntimeError(
650
+ f"Token following /{glyphname} in CharStrings definition "
651
+ f"must be a number, was {nbytes_token}"
652
+ )
653
+ next(tokens) # usually RD or |-
654
+ binary_token = tokens.send(1+nbytes_token.value())
655
+ charstrings[glyphname] = binary_token.value()
656
+
657
+ @staticmethod
658
+ def _parse_encoding(tokens, _data):
659
+ # this only works for encodings that follow the Adobe manual
660
+ # but some old fonts include non-compliant data - we log a warning
661
+ # and return a possibly incomplete encoding
662
+ encoding = {}
663
+ while True:
664
+ token = next(t for t in tokens
665
+ if t.is_keyword('StandardEncoding', 'dup', 'def'))
666
+ if token.is_keyword('StandardEncoding'):
667
+ return _StandardEncoding, token.endpos()
668
+ if token.is_keyword('def'):
669
+ return encoding, token.endpos()
670
+ index_token = next(tokens)
671
+ if not index_token.is_number():
672
+ _log.warning(
673
+ f"Parsing encoding: expected number, got {index_token}"
674
+ )
675
+ continue
676
+ name_token = next(tokens)
677
+ if not name_token.is_slash_name():
678
+ _log.warning(
679
+ f"Parsing encoding: expected slash-name, got {name_token}"
680
+ )
681
+ continue
682
+ encoding[index_token.value()] = name_token.value()
683
+
684
+ @staticmethod
685
+ def _parse_othersubrs(tokens, data):
686
+ init_pos = None
687
+ while True:
688
+ token = next(tokens)
689
+ if init_pos is None:
690
+ init_pos = token.pos
691
+ if token.is_delim():
692
+ _expression(token, tokens, data)
693
+ elif token.is_keyword('def', 'ND', '|-'):
694
+ return data[init_pos:token.endpos()], token.endpos()
695
+
696
+ def transform(self, effects):
697
+ """
698
+ Return a new font that is slanted and/or extended.
699
+
700
+ Parameters
701
+ ----------
702
+ effects : dict
703
+ A dict with optional entries:
704
+
705
+ - 'slant' : float, default: 0
706
+ Tangent of the angle that the font is to be slanted to the
707
+ right. Negative values slant to the left.
708
+ - 'extend' : float, default: 1
709
+ Scaling factor for the font width. Values less than 1 condense
710
+ the glyphs.
711
+
712
+ Returns
713
+ -------
714
+ `Type1Font`
715
+ """
716
+ fontname = self.prop['FontName']
717
+ italicangle = self.prop['ItalicAngle']
718
+
719
+ array = [
720
+ float(x) for x in (self.prop['FontMatrix']
721
+ .lstrip('[').rstrip(']').split())
722
+ ]
723
+ oldmatrix = np.eye(3, 3)
724
+ oldmatrix[0:3, 0] = array[::2]
725
+ oldmatrix[0:3, 1] = array[1::2]
726
+ modifier = np.eye(3, 3)
727
+
728
+ if 'slant' in effects:
729
+ slant = effects['slant']
730
+ fontname += f'_Slant_{int(1000 * slant)}'
731
+ italicangle = round(
732
+ float(italicangle) - np.arctan(slant) / np.pi * 180,
733
+ 5
734
+ )
735
+ modifier[1, 0] = slant
736
+
737
+ if 'extend' in effects:
738
+ extend = effects['extend']
739
+ fontname += f'_Extend_{int(1000 * extend)}'
740
+ modifier[0, 0] = extend
741
+
742
+ newmatrix = np.dot(modifier, oldmatrix)
743
+ array[::2] = newmatrix[0:3, 0]
744
+ array[1::2] = newmatrix[0:3, 1]
745
+ fontmatrix = (
746
+ f"[{' '.join(_format_approx(x, 6) for x in array)}]"
747
+ )
748
+ replacements = (
749
+ [(x, f'/FontName/{fontname} def')
750
+ for x in self._pos['FontName']]
751
+ + [(x, f'/ItalicAngle {italicangle} def')
752
+ for x in self._pos['ItalicAngle']]
753
+ + [(x, f'/FontMatrix {fontmatrix} readonly def')
754
+ for x in self._pos['FontMatrix']]
755
+ + [(x, '') for x in self._pos.get('UniqueID', [])]
756
+ )
757
+
758
+ data = bytearray(self.parts[0])
759
+ data.extend(self.decrypted)
760
+ len0 = len(self.parts[0])
761
+ for (pos0, pos1), value in sorted(replacements, reverse=True):
762
+ data[pos0:pos1] = value.encode('ascii', 'replace')
763
+ if pos0 < len(self.parts[0]):
764
+ if pos1 >= len(self.parts[0]):
765
+ raise RuntimeError(
766
+ f"text to be replaced with {value} spans "
767
+ "the eexec boundary"
768
+ )
769
+ len0 += len(value) - pos1 + pos0
770
+
771
+ data = bytes(data)
772
+ return Type1Font((
773
+ data[:len0],
774
+ self._encrypt(data[len0:], 'eexec'),
775
+ self.parts[2]
776
+ ))
777
+
778
+
779
+ _StandardEncoding = {
780
+ **{ord(letter): letter for letter in string.ascii_letters},
781
+ 0: '.notdef',
782
+ 32: 'space',
783
+ 33: 'exclam',
784
+ 34: 'quotedbl',
785
+ 35: 'numbersign',
786
+ 36: 'dollar',
787
+ 37: 'percent',
788
+ 38: 'ampersand',
789
+ 39: 'quoteright',
790
+ 40: 'parenleft',
791
+ 41: 'parenright',
792
+ 42: 'asterisk',
793
+ 43: 'plus',
794
+ 44: 'comma',
795
+ 45: 'hyphen',
796
+ 46: 'period',
797
+ 47: 'slash',
798
+ 48: 'zero',
799
+ 49: 'one',
800
+ 50: 'two',
801
+ 51: 'three',
802
+ 52: 'four',
803
+ 53: 'five',
804
+ 54: 'six',
805
+ 55: 'seven',
806
+ 56: 'eight',
807
+ 57: 'nine',
808
+ 58: 'colon',
809
+ 59: 'semicolon',
810
+ 60: 'less',
811
+ 61: 'equal',
812
+ 62: 'greater',
813
+ 63: 'question',
814
+ 64: 'at',
815
+ 91: 'bracketleft',
816
+ 92: 'backslash',
817
+ 93: 'bracketright',
818
+ 94: 'asciicircum',
819
+ 95: 'underscore',
820
+ 96: 'quoteleft',
821
+ 123: 'braceleft',
822
+ 124: 'bar',
823
+ 125: 'braceright',
824
+ 126: 'asciitilde',
825
+ 161: 'exclamdown',
826
+ 162: 'cent',
827
+ 163: 'sterling',
828
+ 164: 'fraction',
829
+ 165: 'yen',
830
+ 166: 'florin',
831
+ 167: 'section',
832
+ 168: 'currency',
833
+ 169: 'quotesingle',
834
+ 170: 'quotedblleft',
835
+ 171: 'guillemotleft',
836
+ 172: 'guilsinglleft',
837
+ 173: 'guilsinglright',
838
+ 174: 'fi',
839
+ 175: 'fl',
840
+ 177: 'endash',
841
+ 178: 'dagger',
842
+ 179: 'daggerdbl',
843
+ 180: 'periodcentered',
844
+ 182: 'paragraph',
845
+ 183: 'bullet',
846
+ 184: 'quotesinglbase',
847
+ 185: 'quotedblbase',
848
+ 186: 'quotedblright',
849
+ 187: 'guillemotright',
850
+ 188: 'ellipsis',
851
+ 189: 'perthousand',
852
+ 191: 'questiondown',
853
+ 193: 'grave',
854
+ 194: 'acute',
855
+ 195: 'circumflex',
856
+ 196: 'tilde',
857
+ 197: 'macron',
858
+ 198: 'breve',
859
+ 199: 'dotaccent',
860
+ 200: 'dieresis',
861
+ 202: 'ring',
862
+ 203: 'cedilla',
863
+ 205: 'hungarumlaut',
864
+ 206: 'ogonek',
865
+ 207: 'caron',
866
+ 208: 'emdash',
867
+ 225: 'AE',
868
+ 227: 'ordfeminine',
869
+ 232: 'Lslash',
870
+ 233: 'Oslash',
871
+ 234: 'OE',
872
+ 235: 'ordmasculine',
873
+ 241: 'ae',
874
+ 245: 'dotlessi',
875
+ 248: 'lslash',
876
+ 249: 'oslash',
877
+ 250: 'oe',
878
+ 251: 'germandbls',
879
+ }
lib/python3.12/site-packages/matplotlib/backend_managers.pyi ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from matplotlib import backend_tools, widgets
2
+ from matplotlib.backend_bases import FigureCanvasBase
3
+ from matplotlib.figure import Figure
4
+
5
+ from collections.abc import Callable, Iterable
6
+ from typing import Any, TypeVar
7
+
8
+ class ToolEvent:
9
+ name: str
10
+ sender: Any
11
+ tool: backend_tools.ToolBase
12
+ data: Any
13
+ def __init__(self, name, sender, tool, data: Any | None = ...) -> None: ...
14
+
15
+ class ToolTriggerEvent(ToolEvent):
16
+ canvasevent: ToolEvent
17
+ def __init__(
18
+ self,
19
+ name,
20
+ sender,
21
+ tool,
22
+ canvasevent: ToolEvent | None = ...,
23
+ data: Any | None = ...,
24
+ ) -> None: ...
25
+
26
+ class ToolManagerMessageEvent:
27
+ name: str
28
+ sender: Any
29
+ message: str
30
+ def __init__(self, name: str, sender: Any, message: str) -> None: ...
31
+
32
+ class ToolManager:
33
+ keypresslock: widgets.LockDraw
34
+ messagelock: widgets.LockDraw
35
+ def __init__(self, figure: Figure | None = ...) -> None: ...
36
+ @property
37
+ def canvas(self) -> FigureCanvasBase | None: ...
38
+ @property
39
+ def figure(self) -> Figure | None: ...
40
+ @figure.setter
41
+ def figure(self, figure: Figure) -> None: ...
42
+ def set_figure(self, figure: Figure, update_tools: bool = ...) -> None: ...
43
+ def toolmanager_connect(self, s: str, func: Callable[[ToolEvent], Any]) -> int: ...
44
+ def toolmanager_disconnect(self, cid: int) -> None: ...
45
+ def message_event(self, message: str, sender: Any | None = ...) -> None: ...
46
+ @property
47
+ def active_toggle(self) -> dict[str | None, list[str] | str]: ...
48
+ def get_tool_keymap(self, name: str) -> list[str]: ...
49
+ def update_keymap(self, name: str, key: str | Iterable[str]) -> None: ...
50
+ def remove_tool(self, name: str) -> None: ...
51
+ _T = TypeVar("_T", bound=backend_tools.ToolBase)
52
+ def add_tool(self, name: str, tool: type[_T], *args, **kwargs) -> _T: ...
53
+ def trigger_tool(
54
+ self,
55
+ name: str | backend_tools.ToolBase,
56
+ sender: Any | None = ...,
57
+ canvasevent: ToolEvent | None = ...,
58
+ data: Any | None = ...,
59
+ ) -> None: ...
60
+ @property
61
+ def tools(self) -> dict[str, backend_tools.ToolBase]: ...
62
+ def get_tool(
63
+ self, name: str | backend_tools.ToolBase, warn: bool = ...
64
+ ) -> backend_tools.ToolBase | None: ...
lib/python3.12/site-packages/matplotlib/category.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Plotting of string "category" data: ``plot(['d', 'f', 'a'], [1, 2, 3])`` will
3
+ plot three points with x-axis values of 'd', 'f', 'a'.
4
+
5
+ See :doc:`/gallery/lines_bars_and_markers/categorical_variables` for an
6
+ example.
7
+
8
+ The module uses Matplotlib's `matplotlib.units` mechanism to convert from
9
+ strings to integers and provides a tick locator, a tick formatter, and the
10
+ `.UnitData` class that creates and stores the string-to-integer mapping.
11
+ """
12
+
13
+ from collections import OrderedDict
14
+ import dateutil.parser
15
+ import itertools
16
+ import logging
17
+
18
+ import numpy as np
19
+
20
+ from matplotlib import _api, cbook, ticker, units
21
+
22
+
23
+ _log = logging.getLogger(__name__)
24
+
25
+
26
+ class StrCategoryConverter(units.ConversionInterface):
27
+ @staticmethod
28
+ def convert(value, unit, axis):
29
+ """
30
+ Convert strings in *value* to floats using mapping information stored
31
+ in the *unit* object.
32
+
33
+ Parameters
34
+ ----------
35
+ value : str or iterable
36
+ Value or list of values to be converted.
37
+ unit : `.UnitData`
38
+ An object mapping strings to integers.
39
+ axis : `~matplotlib.axis.Axis`
40
+ The axis on which the converted value is plotted.
41
+
42
+ .. note:: *axis* is unused.
43
+
44
+ Returns
45
+ -------
46
+ float or `~numpy.ndarray` of float
47
+ """
48
+ if unit is None:
49
+ raise ValueError(
50
+ 'Missing category information for StrCategoryConverter; '
51
+ 'this might be caused by unintendedly mixing categorical and '
52
+ 'numeric data')
53
+ StrCategoryConverter._validate_unit(unit)
54
+ # dtype = object preserves numerical pass throughs
55
+ values = np.atleast_1d(np.array(value, dtype=object))
56
+ # force an update so it also does type checking
57
+ unit.update(values)
58
+ s = np.vectorize(unit._mapping.__getitem__, otypes=[float])(values)
59
+ return s if not cbook.is_scalar_or_string(value) else s[0]
60
+
61
+ @staticmethod
62
+ def axisinfo(unit, axis):
63
+ """
64
+ Set the default axis ticks and labels.
65
+
66
+ Parameters
67
+ ----------
68
+ unit : `.UnitData`
69
+ object string unit information for value
70
+ axis : `~matplotlib.axis.Axis`
71
+ axis for which information is being set
72
+
73
+ .. note:: *axis* is not used
74
+
75
+ Returns
76
+ -------
77
+ `~matplotlib.units.AxisInfo`
78
+ Information to support default tick labeling
79
+
80
+ """
81
+ StrCategoryConverter._validate_unit(unit)
82
+ # locator and formatter take mapping dict because
83
+ # args need to be pass by reference for updates
84
+ majloc = StrCategoryLocator(unit._mapping)
85
+ majfmt = StrCategoryFormatter(unit._mapping)
86
+ return units.AxisInfo(majloc=majloc, majfmt=majfmt)
87
+
88
+ @staticmethod
89
+ def default_units(data, axis):
90
+ """
91
+ Set and update the `~matplotlib.axis.Axis` units.
92
+
93
+ Parameters
94
+ ----------
95
+ data : str or iterable of str
96
+ axis : `~matplotlib.axis.Axis`
97
+ axis on which the data is plotted
98
+
99
+ Returns
100
+ -------
101
+ `.UnitData`
102
+ object storing string to integer mapping
103
+ """
104
+ # the conversion call stack is default_units -> axis_info -> convert
105
+ if axis.units is None:
106
+ axis.set_units(UnitData(data))
107
+ else:
108
+ axis.units.update(data)
109
+ return axis.units
110
+
111
+ @staticmethod
112
+ def _validate_unit(unit):
113
+ if not hasattr(unit, '_mapping'):
114
+ raise ValueError(
115
+ f'Provided unit "{unit}" is not valid for a categorical '
116
+ 'converter, as it does not have a _mapping attribute.')
117
+
118
+
119
+ class StrCategoryLocator(ticker.Locator):
120
+ """Tick at every integer mapping of the string data."""
121
+ def __init__(self, units_mapping):
122
+ """
123
+ Parameters
124
+ ----------
125
+ units_mapping : dict
126
+ Mapping of category names (str) to indices (int).
127
+ """
128
+ self._units = units_mapping
129
+
130
+ def __call__(self):
131
+ # docstring inherited
132
+ return list(self._units.values())
133
+
134
+ def tick_values(self, vmin, vmax):
135
+ # docstring inherited
136
+ return self()
137
+
138
+
139
+ class StrCategoryFormatter(ticker.Formatter):
140
+ """String representation of the data at every tick."""
141
+ def __init__(self, units_mapping):
142
+ """
143
+ Parameters
144
+ ----------
145
+ units_mapping : dict
146
+ Mapping of category names (str) to indices (int).
147
+ """
148
+ self._units = units_mapping
149
+
150
+ def __call__(self, x, pos=None):
151
+ # docstring inherited
152
+ return self.format_ticks([x])[0]
153
+
154
+ def format_ticks(self, values):
155
+ # docstring inherited
156
+ r_mapping = {v: self._text(k) for k, v in self._units.items()}
157
+ return [r_mapping.get(round(val), '') for val in values]
158
+
159
+ @staticmethod
160
+ def _text(value):
161
+ """Convert text values into utf-8 or ascii strings."""
162
+ if isinstance(value, bytes):
163
+ value = value.decode(encoding='utf-8')
164
+ elif not isinstance(value, str):
165
+ value = str(value)
166
+ return value
167
+
168
+
169
+ class UnitData:
170
+ def __init__(self, data=None):
171
+ """
172
+ Create mapping between unique categorical values and integer ids.
173
+
174
+ Parameters
175
+ ----------
176
+ data : iterable
177
+ sequence of string values
178
+ """
179
+ self._mapping = OrderedDict()
180
+ self._counter = itertools.count()
181
+ if data is not None:
182
+ self.update(data)
183
+
184
+ @staticmethod
185
+ def _str_is_convertible(val):
186
+ """
187
+ Helper method to check whether a string can be parsed as float or date.
188
+ """
189
+ try:
190
+ float(val)
191
+ except ValueError:
192
+ try:
193
+ dateutil.parser.parse(val)
194
+ except (ValueError, TypeError):
195
+ # TypeError if dateutil >= 2.8.1 else ValueError
196
+ return False
197
+ return True
198
+
199
+ def update(self, data):
200
+ """
201
+ Map new values to integer identifiers.
202
+
203
+ Parameters
204
+ ----------
205
+ data : iterable of str or bytes
206
+
207
+ Raises
208
+ ------
209
+ TypeError
210
+ If elements in *data* are neither str nor bytes.
211
+ """
212
+ data = np.atleast_1d(np.array(data, dtype=object))
213
+ # check if convertible to number:
214
+ convertible = True
215
+ for val in OrderedDict.fromkeys(data):
216
+ # OrderedDict just iterates over unique values in data.
217
+ _api.check_isinstance((str, bytes), value=val)
218
+ if convertible:
219
+ # this will only be called so long as convertible is True.
220
+ convertible = self._str_is_convertible(val)
221
+ if val not in self._mapping:
222
+ self._mapping[val] = next(self._counter)
223
+ if data.size and convertible:
224
+ _log.info('Using categorical units to plot a list of strings '
225
+ 'that are all parsable as floats or dates. If these '
226
+ 'strings should be plotted as numbers, cast to the '
227
+ 'appropriate data type before plotting.')
228
+
229
+
230
+ # Register the converter with Matplotlib's unit framework
231
+ # Intentionally set to a single instance
232
+ units.registry[str] = \
233
+ units.registry[np.str_] = \
234
+ units.registry[bytes] = \
235
+ units.registry[np.bytes_] = StrCategoryConverter()
lib/python3.12/site-packages/matplotlib/cbook.py ADDED
@@ -0,0 +1,2415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A collection of utility functions and classes. Originally, many
3
+ (but not all) were from the Python Cookbook -- hence the name cbook.
4
+ """
5
+
6
+ import collections
7
+ import collections.abc
8
+ import contextlib
9
+ import functools
10
+ import gzip
11
+ import itertools
12
+ import math
13
+ import operator
14
+ import os
15
+ from pathlib import Path
16
+ import shlex
17
+ import subprocess
18
+ import sys
19
+ import time
20
+ import traceback
21
+ import types
22
+ import weakref
23
+
24
+ import numpy as np
25
+
26
+ try:
27
+ from numpy.exceptions import VisibleDeprecationWarning # numpy >= 1.25
28
+ except ImportError:
29
+ from numpy import VisibleDeprecationWarning
30
+
31
+ import matplotlib
32
+ from matplotlib import _api, _c_internal_utils
33
+
34
+
35
+ class _ExceptionInfo:
36
+ """
37
+ A class to carry exception information around.
38
+
39
+ This is used to store and later raise exceptions. It's an alternative to
40
+ directly storing Exception instances that circumvents traceback-related
41
+ issues: caching tracebacks can keep user's objects in local namespaces
42
+ alive indefinitely, which can lead to very surprising memory issues for
43
+ users and result in incorrect tracebacks.
44
+ """
45
+
46
+ def __init__(self, cls, *args):
47
+ self._cls = cls
48
+ self._args = args
49
+
50
+ @classmethod
51
+ def from_exception(cls, exc):
52
+ return cls(type(exc), *exc.args)
53
+
54
+ def to_exception(self):
55
+ return self._cls(*self._args)
56
+
57
+
58
+ def _get_running_interactive_framework():
59
+ """
60
+ Return the interactive framework whose event loop is currently running, if
61
+ any, or "headless" if no event loop can be started, or None.
62
+
63
+ Returns
64
+ -------
65
+ Optional[str]
66
+ One of the following values: "qt", "gtk3", "gtk4", "wx", "tk",
67
+ "macosx", "headless", ``None``.
68
+ """
69
+ # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as
70
+ # entries can also have been explicitly set to None.
71
+ QtWidgets = (
72
+ sys.modules.get("PyQt6.QtWidgets")
73
+ or sys.modules.get("PySide6.QtWidgets")
74
+ or sys.modules.get("PyQt5.QtWidgets")
75
+ or sys.modules.get("PySide2.QtWidgets")
76
+ )
77
+ if QtWidgets and QtWidgets.QApplication.instance():
78
+ return "qt"
79
+ Gtk = sys.modules.get("gi.repository.Gtk")
80
+ if Gtk:
81
+ if Gtk.MAJOR_VERSION == 4:
82
+ from gi.repository import GLib
83
+ if GLib.main_depth():
84
+ return "gtk4"
85
+ if Gtk.MAJOR_VERSION == 3 and Gtk.main_level():
86
+ return "gtk3"
87
+ wx = sys.modules.get("wx")
88
+ if wx and wx.GetApp():
89
+ return "wx"
90
+ tkinter = sys.modules.get("tkinter")
91
+ if tkinter:
92
+ codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__}
93
+ for frame in sys._current_frames().values():
94
+ while frame:
95
+ if frame.f_code in codes:
96
+ return "tk"
97
+ frame = frame.f_back
98
+ # Preemptively break reference cycle between locals and the frame.
99
+ del frame
100
+ macosx = sys.modules.get("matplotlib.backends._macosx")
101
+ if macosx and macosx.event_loop_is_running():
102
+ return "macosx"
103
+ if not _c_internal_utils.display_is_valid():
104
+ return "headless"
105
+ return None
106
+
107
+
108
+ def _exception_printer(exc):
109
+ if _get_running_interactive_framework() in ["headless", None]:
110
+ raise exc
111
+ else:
112
+ traceback.print_exc()
113
+
114
+
115
+ class _StrongRef:
116
+ """
117
+ Wrapper similar to a weakref, but keeping a strong reference to the object.
118
+ """
119
+
120
+ def __init__(self, obj):
121
+ self._obj = obj
122
+
123
+ def __call__(self):
124
+ return self._obj
125
+
126
+ def __eq__(self, other):
127
+ return isinstance(other, _StrongRef) and self._obj == other._obj
128
+
129
+ def __hash__(self):
130
+ return hash(self._obj)
131
+
132
+
133
+ def _weak_or_strong_ref(func, callback):
134
+ """
135
+ Return a `WeakMethod` wrapping *func* if possible, else a `_StrongRef`.
136
+ """
137
+ try:
138
+ return weakref.WeakMethod(func, callback)
139
+ except TypeError:
140
+ return _StrongRef(func)
141
+
142
+
143
+ class _UnhashDict:
144
+ """
145
+ A minimal dict-like class that also supports unhashable keys, storing them
146
+ in a list of key-value pairs.
147
+
148
+ This class only implements the interface needed for `CallbackRegistry`, and
149
+ tries to minimize the overhead for the hashable case.
150
+ """
151
+
152
+ def __init__(self, pairs):
153
+ self._dict = {}
154
+ self._pairs = []
155
+ for k, v in pairs:
156
+ self[k] = v
157
+
158
+ def __setitem__(self, key, value):
159
+ try:
160
+ self._dict[key] = value
161
+ except TypeError:
162
+ for i, (k, v) in enumerate(self._pairs):
163
+ if k == key:
164
+ self._pairs[i] = (key, value)
165
+ break
166
+ else:
167
+ self._pairs.append((key, value))
168
+
169
+ def __getitem__(self, key):
170
+ try:
171
+ return self._dict[key]
172
+ except TypeError:
173
+ pass
174
+ for k, v in self._pairs:
175
+ if k == key:
176
+ return v
177
+ raise KeyError(key)
178
+
179
+ def pop(self, key, *args):
180
+ try:
181
+ if key in self._dict:
182
+ return self._dict.pop(key)
183
+ except TypeError:
184
+ for i, (k, v) in enumerate(self._pairs):
185
+ if k == key:
186
+ del self._pairs[i]
187
+ return v
188
+ if args:
189
+ return args[0]
190
+ raise KeyError(key)
191
+
192
+ def __iter__(self):
193
+ yield from self._dict
194
+ for k, v in self._pairs:
195
+ yield k
196
+
197
+
198
+ class CallbackRegistry:
199
+ """
200
+ Handle registering, processing, blocking, and disconnecting
201
+ for a set of signals and callbacks:
202
+
203
+ >>> def oneat(x):
204
+ ... print('eat', x)
205
+ >>> def ondrink(x):
206
+ ... print('drink', x)
207
+
208
+ >>> from matplotlib.cbook import CallbackRegistry
209
+ >>> callbacks = CallbackRegistry()
210
+
211
+ >>> id_eat = callbacks.connect('eat', oneat)
212
+ >>> id_drink = callbacks.connect('drink', ondrink)
213
+
214
+ >>> callbacks.process('drink', 123)
215
+ drink 123
216
+ >>> callbacks.process('eat', 456)
217
+ eat 456
218
+ >>> callbacks.process('be merry', 456) # nothing will be called
219
+
220
+ >>> callbacks.disconnect(id_eat)
221
+ >>> callbacks.process('eat', 456) # nothing will be called
222
+
223
+ >>> with callbacks.blocked(signal='drink'):
224
+ ... callbacks.process('drink', 123) # nothing will be called
225
+ >>> callbacks.process('drink', 123)
226
+ drink 123
227
+
228
+ In practice, one should always disconnect all callbacks when they are
229
+ no longer needed to avoid dangling references (and thus memory leaks).
230
+ However, real code in Matplotlib rarely does so, and due to its design,
231
+ it is rather difficult to place this kind of code. To get around this,
232
+ and prevent this class of memory leaks, we instead store weak references
233
+ to bound methods only, so when the destination object needs to die, the
234
+ CallbackRegistry won't keep it alive.
235
+
236
+ Parameters
237
+ ----------
238
+ exception_handler : callable, optional
239
+ If not None, *exception_handler* must be a function that takes an
240
+ `Exception` as single parameter. It gets called with any `Exception`
241
+ raised by the callbacks during `CallbackRegistry.process`, and may
242
+ either re-raise the exception or handle it in another manner.
243
+
244
+ The default handler prints the exception (with `traceback.print_exc`) if
245
+ an interactive event loop is running; it re-raises the exception if no
246
+ interactive event loop is running.
247
+
248
+ signals : list, optional
249
+ If not None, *signals* is a list of signals that this registry handles:
250
+ attempting to `process` or to `connect` to a signal not in the list
251
+ throws a `ValueError`. The default, None, does not restrict the
252
+ handled signals.
253
+ """
254
+
255
+ # We maintain two mappings:
256
+ # callbacks: signal -> {cid -> weakref-to-callback}
257
+ # _func_cid_map: {(signal, weakref-to-callback) -> cid}
258
+
259
+ def __init__(self, exception_handler=_exception_printer, *, signals=None):
260
+ self._signals = None if signals is None else list(signals) # Copy it.
261
+ self.exception_handler = exception_handler
262
+ self.callbacks = {}
263
+ self._cid_gen = itertools.count()
264
+ self._func_cid_map = _UnhashDict([])
265
+ # A hidden variable that marks cids that need to be pickled.
266
+ self._pickled_cids = set()
267
+
268
+ def __getstate__(self):
269
+ return {
270
+ **vars(self),
271
+ # In general, callbacks may not be pickled, so we just drop them,
272
+ # unless directed otherwise by self._pickled_cids.
273
+ "callbacks": {s: {cid: proxy() for cid, proxy in d.items()
274
+ if cid in self._pickled_cids}
275
+ for s, d in self.callbacks.items()},
276
+ # It is simpler to reconstruct this from callbacks in __setstate__.
277
+ "_func_cid_map": None,
278
+ "_cid_gen": next(self._cid_gen)
279
+ }
280
+
281
+ def __setstate__(self, state):
282
+ cid_count = state.pop('_cid_gen')
283
+ vars(self).update(state)
284
+ self.callbacks = {
285
+ s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s))
286
+ for cid, func in d.items()}
287
+ for s, d in self.callbacks.items()}
288
+ self._func_cid_map = _UnhashDict(
289
+ ((s, proxy), cid)
290
+ for s, d in self.callbacks.items() for cid, proxy in d.items())
291
+ self._cid_gen = itertools.count(cid_count)
292
+
293
+ def connect(self, signal, func):
294
+ """Register *func* to be called when signal *signal* is generated."""
295
+ if self._signals is not None:
296
+ _api.check_in_list(self._signals, signal=signal)
297
+ proxy = _weak_or_strong_ref(func, functools.partial(self._remove_proxy, signal))
298
+ try:
299
+ return self._func_cid_map[signal, proxy]
300
+ except KeyError:
301
+ cid = self._func_cid_map[signal, proxy] = next(self._cid_gen)
302
+ self.callbacks.setdefault(signal, {})[cid] = proxy
303
+ return cid
304
+
305
+ def _connect_picklable(self, signal, func):
306
+ """
307
+ Like `.connect`, but the callback is kept when pickling/unpickling.
308
+
309
+ Currently internal-use only.
310
+ """
311
+ cid = self.connect(signal, func)
312
+ self._pickled_cids.add(cid)
313
+ return cid
314
+
315
+ # Keep a reference to sys.is_finalizing, as sys may have been cleared out
316
+ # at that point.
317
+ def _remove_proxy(self, signal, proxy, *, _is_finalizing=sys.is_finalizing):
318
+ if _is_finalizing():
319
+ # Weakrefs can't be properly torn down at that point anymore.
320
+ return
321
+ cid = self._func_cid_map.pop((signal, proxy), None)
322
+ if cid is not None:
323
+ del self.callbacks[signal][cid]
324
+ self._pickled_cids.discard(cid)
325
+ else: # Not found
326
+ return
327
+ if len(self.callbacks[signal]) == 0: # Clean up empty dicts
328
+ del self.callbacks[signal]
329
+
330
+ def disconnect(self, cid):
331
+ """
332
+ Disconnect the callback registered with callback id *cid*.
333
+
334
+ No error is raised if such a callback does not exist.
335
+ """
336
+ self._pickled_cids.discard(cid)
337
+ for signal, proxy in self._func_cid_map:
338
+ if self._func_cid_map[signal, proxy] == cid:
339
+ break
340
+ else: # Not found
341
+ return
342
+ assert self.callbacks[signal][cid] == proxy
343
+ del self.callbacks[signal][cid]
344
+ self._func_cid_map.pop((signal, proxy))
345
+ if len(self.callbacks[signal]) == 0: # Clean up empty dicts
346
+ del self.callbacks[signal]
347
+
348
+ def process(self, s, *args, **kwargs):
349
+ """
350
+ Process signal *s*.
351
+
352
+ All of the functions registered to receive callbacks on *s* will be
353
+ called with ``*args`` and ``**kwargs``.
354
+ """
355
+ if self._signals is not None:
356
+ _api.check_in_list(self._signals, signal=s)
357
+ for ref in list(self.callbacks.get(s, {}).values()):
358
+ func = ref()
359
+ if func is not None:
360
+ try:
361
+ func(*args, **kwargs)
362
+ # this does not capture KeyboardInterrupt, SystemExit,
363
+ # and GeneratorExit
364
+ except Exception as exc:
365
+ if self.exception_handler is not None:
366
+ self.exception_handler(exc)
367
+ else:
368
+ raise
369
+
370
+ @contextlib.contextmanager
371
+ def blocked(self, *, signal=None):
372
+ """
373
+ Block callback signals from being processed.
374
+
375
+ A context manager to temporarily block/disable callback signals
376
+ from being processed by the registered listeners.
377
+
378
+ Parameters
379
+ ----------
380
+ signal : str, optional
381
+ The callback signal to block. The default is to block all signals.
382
+ """
383
+ orig = self.callbacks
384
+ try:
385
+ if signal is None:
386
+ # Empty out the callbacks
387
+ self.callbacks = {}
388
+ else:
389
+ # Only remove the specific signal
390
+ self.callbacks = {k: orig[k] for k in orig if k != signal}
391
+ yield
392
+ finally:
393
+ self.callbacks = orig
394
+
395
+
396
+ class silent_list(list):
397
+ """
398
+ A list with a short ``repr()``.
399
+
400
+ This is meant to be used for a homogeneous list of artists, so that they
401
+ don't cause long, meaningless output.
402
+
403
+ Instead of ::
404
+
405
+ [<matplotlib.lines.Line2D object at 0x7f5749fed3c8>,
406
+ <matplotlib.lines.Line2D object at 0x7f5749fed4e0>,
407
+ <matplotlib.lines.Line2D object at 0x7f5758016550>]
408
+
409
+ one will get ::
410
+
411
+ <a list of 3 Line2D objects>
412
+
413
+ If ``self.type`` is None, the type name is obtained from the first item in
414
+ the list (if any).
415
+ """
416
+
417
+ def __init__(self, type, seq=None):
418
+ self.type = type
419
+ if seq is not None:
420
+ self.extend(seq)
421
+
422
+ def __repr__(self):
423
+ if self.type is not None or len(self) != 0:
424
+ tp = self.type if self.type is not None else type(self[0]).__name__
425
+ return f"<a list of {len(self)} {tp} objects>"
426
+ else:
427
+ return "<an empty list>"
428
+
429
+
430
+ def _local_over_kwdict(
431
+ local_var, kwargs, *keys,
432
+ warning_cls=_api.MatplotlibDeprecationWarning):
433
+ out = local_var
434
+ for key in keys:
435
+ kwarg_val = kwargs.pop(key, None)
436
+ if kwarg_val is not None:
437
+ if out is None:
438
+ out = kwarg_val
439
+ else:
440
+ _api.warn_external(f'"{key}" keyword argument will be ignored',
441
+ warning_cls)
442
+ return out
443
+
444
+
445
+ def strip_math(s):
446
+ """
447
+ Remove latex formatting from mathtext.
448
+
449
+ Only handles fully math and fully non-math strings.
450
+ """
451
+ if len(s) >= 2 and s[0] == s[-1] == "$":
452
+ s = s[1:-1]
453
+ for tex, plain in [
454
+ (r"\times", "x"), # Specifically for Formatter support.
455
+ (r"\mathdefault", ""),
456
+ (r"\rm", ""),
457
+ (r"\cal", ""),
458
+ (r"\tt", ""),
459
+ (r"\it", ""),
460
+ ("\\", ""),
461
+ ("{", ""),
462
+ ("}", ""),
463
+ ]:
464
+ s = s.replace(tex, plain)
465
+ return s
466
+
467
+
468
+ def _strip_comment(s):
469
+ """Strip everything from the first unquoted #."""
470
+ pos = 0
471
+ while True:
472
+ quote_pos = s.find('"', pos)
473
+ hash_pos = s.find('#', pos)
474
+ if quote_pos < 0:
475
+ without_comment = s if hash_pos < 0 else s[:hash_pos]
476
+ return without_comment.strip()
477
+ elif 0 <= hash_pos < quote_pos:
478
+ return s[:hash_pos].strip()
479
+ else:
480
+ closing_quote_pos = s.find('"', quote_pos + 1)
481
+ if closing_quote_pos < 0:
482
+ raise ValueError(
483
+ f"Missing closing quote in: {s!r}. If you need a double-"
484
+ 'quote inside a string, use escaping: e.g. "the \" char"')
485
+ pos = closing_quote_pos + 1 # behind closing quote
486
+
487
+
488
+ def is_writable_file_like(obj):
489
+ """Return whether *obj* looks like a file object with a *write* method."""
490
+ return callable(getattr(obj, 'write', None))
491
+
492
+
493
+ def file_requires_unicode(x):
494
+ """
495
+ Return whether the given writable file-like object requires Unicode to be
496
+ written to it.
497
+ """
498
+ try:
499
+ x.write(b'')
500
+ except TypeError:
501
+ return True
502
+ else:
503
+ return False
504
+
505
+
506
+ def to_filehandle(fname, flag='r', return_opened=False, encoding=None):
507
+ """
508
+ Convert a path to an open file handle or pass-through a file-like object.
509
+
510
+ Consider using `open_file_cm` instead, as it allows one to properly close
511
+ newly created file objects more easily.
512
+
513
+ Parameters
514
+ ----------
515
+ fname : str or path-like or file-like
516
+ If `str` or `os.PathLike`, the file is opened using the flags specified
517
+ by *flag* and *encoding*. If a file-like object, it is passed through.
518
+ flag : str, default: 'r'
519
+ Passed as the *mode* argument to `open` when *fname* is `str` or
520
+ `os.PathLike`; ignored if *fname* is file-like.
521
+ return_opened : bool, default: False
522
+ If True, return both the file object and a boolean indicating whether
523
+ this was a new file (that the caller needs to close). If False, return
524
+ only the new file.
525
+ encoding : str or None, default: None
526
+ Passed as the *mode* argument to `open` when *fname* is `str` or
527
+ `os.PathLike`; ignored if *fname* is file-like.
528
+
529
+ Returns
530
+ -------
531
+ fh : file-like
532
+ opened : bool
533
+ *opened* is only returned if *return_opened* is True.
534
+ """
535
+ if isinstance(fname, os.PathLike):
536
+ fname = os.fspath(fname)
537
+ if isinstance(fname, str):
538
+ if fname.endswith('.gz'):
539
+ fh = gzip.open(fname, flag)
540
+ elif fname.endswith('.bz2'):
541
+ # python may not be compiled with bz2 support,
542
+ # bury import until we need it
543
+ import bz2
544
+ fh = bz2.BZ2File(fname, flag)
545
+ else:
546
+ fh = open(fname, flag, encoding=encoding)
547
+ opened = True
548
+ elif hasattr(fname, 'seek'):
549
+ fh = fname
550
+ opened = False
551
+ else:
552
+ raise ValueError('fname must be a PathLike or file handle')
553
+ if return_opened:
554
+ return fh, opened
555
+ return fh
556
+
557
+
558
+ def open_file_cm(path_or_file, mode="r", encoding=None):
559
+ r"""Pass through file objects and context-manage path-likes."""
560
+ fh, opened = to_filehandle(path_or_file, mode, True, encoding)
561
+ return fh if opened else contextlib.nullcontext(fh)
562
+
563
+
564
+ def is_scalar_or_string(val):
565
+ """Return whether the given object is a scalar or string like."""
566
+ return isinstance(val, str) or not np.iterable(val)
567
+
568
+
569
+ def get_sample_data(fname, asfileobj=True):
570
+ """
571
+ Return a sample data file. *fname* is a path relative to the
572
+ :file:`mpl-data/sample_data` directory. If *asfileobj* is `True`
573
+ return a file object, otherwise just a file path.
574
+
575
+ Sample data files are stored in the 'mpl-data/sample_data' directory within
576
+ the Matplotlib package.
577
+
578
+ If the filename ends in .gz, the file is implicitly ungzipped. If the
579
+ filename ends with .npy or .npz, and *asfileobj* is `True`, the file is
580
+ loaded with `numpy.load`.
581
+ """
582
+ path = _get_data_path('sample_data', fname)
583
+ if asfileobj:
584
+ suffix = path.suffix.lower()
585
+ if suffix == '.gz':
586
+ return gzip.open(path)
587
+ elif suffix in ['.npy', '.npz']:
588
+ return np.load(path)
589
+ elif suffix in ['.csv', '.xrc', '.txt']:
590
+ return path.open('r')
591
+ else:
592
+ return path.open('rb')
593
+ else:
594
+ return str(path)
595
+
596
+
597
+ def _get_data_path(*args):
598
+ """
599
+ Return the `pathlib.Path` to a resource file provided by Matplotlib.
600
+
601
+ ``*args`` specify a path relative to the base data path.
602
+ """
603
+ return Path(matplotlib.get_data_path(), *args)
604
+
605
+
606
+ def flatten(seq, scalarp=is_scalar_or_string):
607
+ """
608
+ Return a generator of flattened nested containers.
609
+
610
+ For example:
611
+
612
+ >>> from matplotlib.cbook import flatten
613
+ >>> l = (('John', ['Hunter']), (1, 23), [[([42, (5, 23)], )]])
614
+ >>> print(list(flatten(l)))
615
+ ['John', 'Hunter', 1, 23, 42, 5, 23]
616
+
617
+ By: Composite of Holger Krekel and Luther Blissett
618
+ From: https://code.activestate.com/recipes/121294-simple-generator-for-flattening-nested-containers/
619
+ and Recipe 1.12 in cookbook
620
+ """ # noqa: E501
621
+ for item in seq:
622
+ if scalarp(item) or item is None:
623
+ yield item
624
+ else:
625
+ yield from flatten(item, scalarp)
626
+
627
+
628
+ class _Stack:
629
+ """
630
+ Stack of elements with a movable cursor.
631
+
632
+ Mimics home/back/forward in a web browser.
633
+ """
634
+
635
+ def __init__(self):
636
+ self._pos = -1
637
+ self._elements = []
638
+
639
+ def clear(self):
640
+ """Empty the stack."""
641
+ self._pos = -1
642
+ self._elements = []
643
+
644
+ def __call__(self):
645
+ """Return the current element, or None."""
646
+ return self._elements[self._pos] if self._elements else None
647
+
648
+ def __len__(self):
649
+ return len(self._elements)
650
+
651
+ def __getitem__(self, ind):
652
+ return self._elements[ind]
653
+
654
+ def forward(self):
655
+ """Move the position forward and return the current element."""
656
+ self._pos = min(self._pos + 1, len(self._elements) - 1)
657
+ return self()
658
+
659
+ def back(self):
660
+ """Move the position back and return the current element."""
661
+ self._pos = max(self._pos - 1, 0)
662
+ return self()
663
+
664
+ def push(self, o):
665
+ """
666
+ Push *o* to the stack after the current position, and return *o*.
667
+
668
+ Discard all later elements.
669
+ """
670
+ self._elements[self._pos + 1:] = [o]
671
+ self._pos = len(self._elements) - 1
672
+ return o
673
+
674
+ def home(self):
675
+ """
676
+ Push the first element onto the top of the stack.
677
+
678
+ The first element is returned.
679
+ """
680
+ return self.push(self._elements[0]) if self._elements else None
681
+
682
+
683
+ def safe_masked_invalid(x, copy=False):
684
+ x = np.array(x, subok=True, copy=copy)
685
+ if not x.dtype.isnative:
686
+ # If we have already made a copy, do the byteswap in place, else make a
687
+ # copy with the byte order swapped.
688
+ # Swap to native order.
689
+ x = x.byteswap(inplace=copy).view(x.dtype.newbyteorder('N'))
690
+ try:
691
+ xm = np.ma.masked_where(~(np.isfinite(x)), x, copy=False)
692
+ except TypeError:
693
+ return x
694
+ return xm
695
+
696
+
697
+ def print_cycles(objects, outstream=sys.stdout, show_progress=False):
698
+ """
699
+ Print loops of cyclic references in the given *objects*.
700
+
701
+ It is often useful to pass in ``gc.garbage`` to find the cycles that are
702
+ preventing some objects from being garbage collected.
703
+
704
+ Parameters
705
+ ----------
706
+ objects
707
+ A list of objects to find cycles in.
708
+ outstream
709
+ The stream for output.
710
+ show_progress : bool
711
+ If True, print the number of objects reached as they are found.
712
+ """
713
+ import gc
714
+
715
+ def print_path(path):
716
+ for i, step in enumerate(path):
717
+ # next "wraps around"
718
+ next = path[(i + 1) % len(path)]
719
+
720
+ outstream.write(" %s -- " % type(step))
721
+ if isinstance(step, dict):
722
+ for key, val in step.items():
723
+ if val is next:
724
+ outstream.write(f"[{key!r}]")
725
+ break
726
+ if key is next:
727
+ outstream.write(f"[key] = {val!r}")
728
+ break
729
+ elif isinstance(step, list):
730
+ outstream.write("[%d]" % step.index(next))
731
+ elif isinstance(step, tuple):
732
+ outstream.write("( tuple )")
733
+ else:
734
+ outstream.write(repr(step))
735
+ outstream.write(" ->\n")
736
+ outstream.write("\n")
737
+
738
+ def recurse(obj, start, all, current_path):
739
+ if show_progress:
740
+ outstream.write("%d\r" % len(all))
741
+
742
+ all[id(obj)] = None
743
+
744
+ referents = gc.get_referents(obj)
745
+ for referent in referents:
746
+ # If we've found our way back to the start, this is
747
+ # a cycle, so print it out
748
+ if referent is start:
749
+ print_path(current_path)
750
+
751
+ # Don't go back through the original list of objects, or
752
+ # through temporary references to the object, since those
753
+ # are just an artifact of the cycle detector itself.
754
+ elif referent is objects or isinstance(referent, types.FrameType):
755
+ continue
756
+
757
+ # We haven't seen this object before, so recurse
758
+ elif id(referent) not in all:
759
+ recurse(referent, start, all, current_path + [obj])
760
+
761
+ for obj in objects:
762
+ outstream.write(f"Examining: {obj!r}\n")
763
+ recurse(obj, obj, {}, [])
764
+
765
+
766
+ class Grouper:
767
+ """
768
+ A disjoint-set data structure.
769
+
770
+ Objects can be joined using :meth:`join`, tested for connectedness
771
+ using :meth:`joined`, and all disjoint sets can be retrieved by
772
+ using the object as an iterator.
773
+
774
+ The objects being joined must be hashable and weak-referenceable.
775
+
776
+ Examples
777
+ --------
778
+ >>> from matplotlib.cbook import Grouper
779
+ >>> class Foo:
780
+ ... def __init__(self, s):
781
+ ... self.s = s
782
+ ... def __repr__(self):
783
+ ... return self.s
784
+ ...
785
+ >>> a, b, c, d, e, f = [Foo(x) for x in 'abcdef']
786
+ >>> grp = Grouper()
787
+ >>> grp.join(a, b)
788
+ >>> grp.join(b, c)
789
+ >>> grp.join(d, e)
790
+ >>> list(grp)
791
+ [[a, b, c], [d, e]]
792
+ >>> grp.joined(a, b)
793
+ True
794
+ >>> grp.joined(a, c)
795
+ True
796
+ >>> grp.joined(a, d)
797
+ False
798
+ """
799
+
800
+ def __init__(self, init=()):
801
+ self._mapping = weakref.WeakKeyDictionary(
802
+ {x: weakref.WeakSet([x]) for x in init})
803
+ self._ordering = weakref.WeakKeyDictionary()
804
+ for x in init:
805
+ if x not in self._ordering:
806
+ self._ordering[x] = len(self._ordering)
807
+ self._next_order = len(self._ordering) # Plain int to simplify pickling.
808
+
809
+ def __getstate__(self):
810
+ return {
811
+ **vars(self),
812
+ # Convert weak refs to strong ones.
813
+ "_mapping": {k: set(v) for k, v in self._mapping.items()},
814
+ "_ordering": {**self._ordering},
815
+ }
816
+
817
+ def __setstate__(self, state):
818
+ vars(self).update(state)
819
+ # Convert strong refs to weak ones.
820
+ self._mapping = weakref.WeakKeyDictionary(
821
+ {k: weakref.WeakSet(v) for k, v in self._mapping.items()})
822
+ self._ordering = weakref.WeakKeyDictionary(self._ordering)
823
+
824
+ def __contains__(self, item):
825
+ return item in self._mapping
826
+
827
+ def join(self, a, *args):
828
+ """
829
+ Join given arguments into the same set. Accepts one or more arguments.
830
+ """
831
+ mapping = self._mapping
832
+ try:
833
+ set_a = mapping[a]
834
+ except KeyError:
835
+ set_a = mapping[a] = weakref.WeakSet([a])
836
+ self._ordering[a] = self._next_order
837
+ self._next_order += 1
838
+ for arg in args:
839
+ try:
840
+ set_b = mapping[arg]
841
+ except KeyError:
842
+ set_b = mapping[arg] = weakref.WeakSet([arg])
843
+ self._ordering[arg] = self._next_order
844
+ self._next_order += 1
845
+ if set_b is not set_a:
846
+ if len(set_b) > len(set_a):
847
+ set_a, set_b = set_b, set_a
848
+ set_a.update(set_b)
849
+ for elem in set_b:
850
+ mapping[elem] = set_a
851
+
852
+ def joined(self, a, b):
853
+ """Return whether *a* and *b* are members of the same set."""
854
+ return (self._mapping.get(a, object()) is self._mapping.get(b))
855
+
856
+ def remove(self, a):
857
+ """Remove *a* from the grouper, doing nothing if it is not there."""
858
+ self._mapping.pop(a, {a}).remove(a)
859
+ self._ordering.pop(a, None)
860
+
861
+ def __iter__(self):
862
+ """
863
+ Iterate over each of the disjoint sets as a list.
864
+
865
+ The iterator is invalid if interleaved with calls to join().
866
+ """
867
+ unique_groups = {id(group): group for group in self._mapping.values()}
868
+ for group in unique_groups.values():
869
+ yield sorted(group, key=self._ordering.__getitem__)
870
+
871
+ def get_siblings(self, a):
872
+ """Return all of the items joined with *a*, including itself."""
873
+ siblings = self._mapping.get(a, [a])
874
+ return sorted(siblings, key=self._ordering.get)
875
+
876
+
877
+ class GrouperView:
878
+ """Immutable view over a `.Grouper`."""
879
+
880
+ def __init__(self, grouper): self._grouper = grouper
881
+ def __contains__(self, item): return item in self._grouper
882
+ def __iter__(self): return iter(self._grouper)
883
+
884
+ def joined(self, a, b):
885
+ """
886
+ Return whether *a* and *b* are members of the same set.
887
+ """
888
+ return self._grouper.joined(a, b)
889
+
890
+ def get_siblings(self, a):
891
+ """
892
+ Return all of the items joined with *a*, including itself.
893
+ """
894
+ return self._grouper.get_siblings(a)
895
+
896
+
897
+ def simple_linear_interpolation(a, steps):
898
+ """
899
+ Resample an array with ``steps - 1`` points between original point pairs.
900
+
901
+ Along each column of *a*, ``(steps - 1)`` points are introduced between
902
+ each original values; the values are linearly interpolated.
903
+
904
+ Parameters
905
+ ----------
906
+ a : array, shape (n, ...)
907
+ steps : int
908
+
909
+ Returns
910
+ -------
911
+ array
912
+ shape ``((n - 1) * steps + 1, ...)``
913
+ """
914
+ fps = a.reshape((len(a), -1))
915
+ xp = np.arange(len(a)) * steps
916
+ x = np.arange((len(a) - 1) * steps + 1)
917
+ return (np.column_stack([np.interp(x, xp, fp) for fp in fps.T])
918
+ .reshape((len(x),) + a.shape[1:]))
919
+
920
+
921
+ def delete_masked_points(*args):
922
+ """
923
+ Find all masked and/or non-finite points in a set of arguments,
924
+ and return the arguments with only the unmasked points remaining.
925
+
926
+ Arguments can be in any of 5 categories:
927
+
928
+ 1) 1-D masked arrays
929
+ 2) 1-D ndarrays
930
+ 3) ndarrays with more than one dimension
931
+ 4) other non-string iterables
932
+ 5) anything else
933
+
934
+ The first argument must be in one of the first four categories;
935
+ any argument with a length differing from that of the first
936
+ argument (and hence anything in category 5) then will be
937
+ passed through unchanged.
938
+
939
+ Masks are obtained from all arguments of the correct length
940
+ in categories 1, 2, and 4; a point is bad if masked in a masked
941
+ array or if it is a nan or inf. No attempt is made to
942
+ extract a mask from categories 2, 3, and 4 if `numpy.isfinite`
943
+ does not yield a Boolean array.
944
+
945
+ All input arguments that are not passed unchanged are returned
946
+ as ndarrays after removing the points or rows corresponding to
947
+ masks in any of the arguments.
948
+
949
+ A vastly simpler version of this function was originally
950
+ written as a helper for Axes.scatter().
951
+
952
+ """
953
+ if not len(args):
954
+ return ()
955
+ if is_scalar_or_string(args[0]):
956
+ raise ValueError("First argument must be a sequence")
957
+ nrecs = len(args[0])
958
+ margs = []
959
+ seqlist = [False] * len(args)
960
+ for i, x in enumerate(args):
961
+ if not isinstance(x, str) and np.iterable(x) and len(x) == nrecs:
962
+ seqlist[i] = True
963
+ if isinstance(x, np.ma.MaskedArray):
964
+ if x.ndim > 1:
965
+ raise ValueError("Masked arrays must be 1-D")
966
+ else:
967
+ x = np.asarray(x)
968
+ margs.append(x)
969
+ masks = [] # List of masks that are True where good.
970
+ for i, x in enumerate(margs):
971
+ if seqlist[i]:
972
+ if x.ndim > 1:
973
+ continue # Don't try to get nan locations unless 1-D.
974
+ if isinstance(x, np.ma.MaskedArray):
975
+ masks.append(~np.ma.getmaskarray(x)) # invert the mask
976
+ xd = x.data
977
+ else:
978
+ xd = x
979
+ try:
980
+ mask = np.isfinite(xd)
981
+ if isinstance(mask, np.ndarray):
982
+ masks.append(mask)
983
+ except Exception: # Fixme: put in tuple of possible exceptions?
984
+ pass
985
+ if len(masks):
986
+ mask = np.logical_and.reduce(masks)
987
+ igood = mask.nonzero()[0]
988
+ if len(igood) < nrecs:
989
+ for i, x in enumerate(margs):
990
+ if seqlist[i]:
991
+ margs[i] = x[igood]
992
+ for i, x in enumerate(margs):
993
+ if seqlist[i] and isinstance(x, np.ma.MaskedArray):
994
+ margs[i] = x.filled()
995
+ return margs
996
+
997
+
998
+ def _combine_masks(*args):
999
+ """
1000
+ Find all masked and/or non-finite points in a set of arguments,
1001
+ and return the arguments as masked arrays with a common mask.
1002
+
1003
+ Arguments can be in any of 5 categories:
1004
+
1005
+ 1) 1-D masked arrays
1006
+ 2) 1-D ndarrays
1007
+ 3) ndarrays with more than one dimension
1008
+ 4) other non-string iterables
1009
+ 5) anything else
1010
+
1011
+ The first argument must be in one of the first four categories;
1012
+ any argument with a length differing from that of the first
1013
+ argument (and hence anything in category 5) then will be
1014
+ passed through unchanged.
1015
+
1016
+ Masks are obtained from all arguments of the correct length
1017
+ in categories 1, 2, and 4; a point is bad if masked in a masked
1018
+ array or if it is a nan or inf. No attempt is made to
1019
+ extract a mask from categories 2 and 4 if `numpy.isfinite`
1020
+ does not yield a Boolean array. Category 3 is included to
1021
+ support RGB or RGBA ndarrays, which are assumed to have only
1022
+ valid values and which are passed through unchanged.
1023
+
1024
+ All input arguments that are not passed unchanged are returned
1025
+ as masked arrays if any masked points are found, otherwise as
1026
+ ndarrays.
1027
+
1028
+ """
1029
+ if not len(args):
1030
+ return ()
1031
+ if is_scalar_or_string(args[0]):
1032
+ raise ValueError("First argument must be a sequence")
1033
+ nrecs = len(args[0])
1034
+ margs = [] # Output args; some may be modified.
1035
+ seqlist = [False] * len(args) # Flags: True if output will be masked.
1036
+ masks = [] # List of masks.
1037
+ for i, x in enumerate(args):
1038
+ if is_scalar_or_string(x) or len(x) != nrecs:
1039
+ margs.append(x) # Leave it unmodified.
1040
+ else:
1041
+ if isinstance(x, np.ma.MaskedArray) and x.ndim > 1:
1042
+ raise ValueError("Masked arrays must be 1-D")
1043
+ try:
1044
+ x = np.asanyarray(x)
1045
+ except (VisibleDeprecationWarning, ValueError):
1046
+ # NumPy 1.19 raises a warning about ragged arrays, but we want
1047
+ # to accept basically anything here.
1048
+ x = np.asanyarray(x, dtype=object)
1049
+ if x.ndim == 1:
1050
+ x = safe_masked_invalid(x)
1051
+ seqlist[i] = True
1052
+ if np.ma.is_masked(x):
1053
+ masks.append(np.ma.getmaskarray(x))
1054
+ margs.append(x) # Possibly modified.
1055
+ if len(masks):
1056
+ mask = np.logical_or.reduce(masks)
1057
+ for i, x in enumerate(margs):
1058
+ if seqlist[i]:
1059
+ margs[i] = np.ma.array(x, mask=mask)
1060
+ return margs
1061
+
1062
+
1063
+ def _broadcast_with_masks(*args, compress=False):
1064
+ """
1065
+ Broadcast inputs, combining all masked arrays.
1066
+
1067
+ Parameters
1068
+ ----------
1069
+ *args : array-like
1070
+ The inputs to broadcast.
1071
+ compress : bool, default: False
1072
+ Whether to compress the masked arrays. If False, the masked values
1073
+ are replaced by NaNs.
1074
+
1075
+ Returns
1076
+ -------
1077
+ list of array-like
1078
+ The broadcasted and masked inputs.
1079
+ """
1080
+ # extract the masks, if any
1081
+ masks = [k.mask for k in args if isinstance(k, np.ma.MaskedArray)]
1082
+ # broadcast to match the shape
1083
+ bcast = np.broadcast_arrays(*args, *masks)
1084
+ inputs = bcast[:len(args)]
1085
+ masks = bcast[len(args):]
1086
+ if masks:
1087
+ # combine the masks into one
1088
+ mask = np.logical_or.reduce(masks)
1089
+ # put mask on and compress
1090
+ if compress:
1091
+ inputs = [np.ma.array(k, mask=mask).compressed()
1092
+ for k in inputs]
1093
+ else:
1094
+ inputs = [np.ma.array(k, mask=mask, dtype=float).filled(np.nan).ravel()
1095
+ for k in inputs]
1096
+ else:
1097
+ inputs = [np.ravel(k) for k in inputs]
1098
+ return inputs
1099
+
1100
+
1101
+ def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None, autorange=False):
1102
+ r"""
1103
+ Return a list of dictionaries of statistics used to draw a series of box
1104
+ and whisker plots using `~.Axes.bxp`.
1105
+
1106
+ Parameters
1107
+ ----------
1108
+ X : array-like
1109
+ Data that will be represented in the boxplots. Should have 2 or
1110
+ fewer dimensions.
1111
+
1112
+ whis : float or (float, float), default: 1.5
1113
+ The position of the whiskers.
1114
+
1115
+ If a float, the lower whisker is at the lowest datum above
1116
+ ``Q1 - whis*(Q3-Q1)``, and the upper whisker at the highest datum below
1117
+ ``Q3 + whis*(Q3-Q1)``, where Q1 and Q3 are the first and third
1118
+ quartiles. The default value of ``whis = 1.5`` corresponds to Tukey's
1119
+ original definition of boxplots.
1120
+
1121
+ If a pair of floats, they indicate the percentiles at which to draw the
1122
+ whiskers (e.g., (5, 95)). In particular, setting this to (0, 100)
1123
+ results in whiskers covering the whole range of the data.
1124
+
1125
+ In the edge case where ``Q1 == Q3``, *whis* is automatically set to
1126
+ (0, 100) (cover the whole range of the data) if *autorange* is True.
1127
+
1128
+ Beyond the whiskers, data are considered outliers and are plotted as
1129
+ individual points.
1130
+
1131
+ bootstrap : int, optional
1132
+ Number of times the confidence intervals around the median
1133
+ should be bootstrapped (percentile method).
1134
+
1135
+ labels : list of str, optional
1136
+ Labels for each dataset. Length must be compatible with
1137
+ dimensions of *X*.
1138
+
1139
+ autorange : bool, optional (False)
1140
+ When `True` and the data are distributed such that the 25th and 75th
1141
+ percentiles are equal, ``whis`` is set to (0, 100) such that the
1142
+ whisker ends are at the minimum and maximum of the data.
1143
+
1144
+ Returns
1145
+ -------
1146
+ list of dict
1147
+ A list of dictionaries containing the results for each column
1148
+ of data. Keys of each dictionary are the following:
1149
+
1150
+ ======== ===================================
1151
+ Key Value Description
1152
+ ======== ===================================
1153
+ label tick label for the boxplot
1154
+ mean arithmetic mean value
1155
+ med 50th percentile
1156
+ q1 first quartile (25th percentile)
1157
+ q3 third quartile (75th percentile)
1158
+ iqr interquartile range
1159
+ cilo lower notch around the median
1160
+ cihi upper notch around the median
1161
+ whislo end of the lower whisker
1162
+ whishi end of the upper whisker
1163
+ fliers outliers
1164
+ ======== ===================================
1165
+
1166
+ Notes
1167
+ -----
1168
+ Non-bootstrapping approach to confidence interval uses Gaussian-based
1169
+ asymptotic approximation:
1170
+
1171
+ .. math::
1172
+
1173
+ \mathrm{med} \pm 1.57 \times \frac{\mathrm{iqr}}{\sqrt{N}}
1174
+
1175
+ General approach from:
1176
+ McGill, R., Tukey, J.W., and Larsen, W.A. (1978) "Variations of
1177
+ Boxplots", The American Statistician, 32:12-16.
1178
+ """
1179
+
1180
+ def _bootstrap_median(data, N=5000):
1181
+ # determine 95% confidence intervals of the median
1182
+ M = len(data)
1183
+ percentiles = [2.5, 97.5]
1184
+
1185
+ bs_index = np.random.randint(M, size=(N, M))
1186
+ bsData = data[bs_index]
1187
+ estimate = np.median(bsData, axis=1, overwrite_input=True)
1188
+
1189
+ CI = np.percentile(estimate, percentiles)
1190
+ return CI
1191
+
1192
+ def _compute_conf_interval(data, med, iqr, bootstrap):
1193
+ if bootstrap is not None:
1194
+ # Do a bootstrap estimate of notch locations.
1195
+ # get conf. intervals around median
1196
+ CI = _bootstrap_median(data, N=bootstrap)
1197
+ notch_min = CI[0]
1198
+ notch_max = CI[1]
1199
+ else:
1200
+
1201
+ N = len(data)
1202
+ notch_min = med - 1.57 * iqr / np.sqrt(N)
1203
+ notch_max = med + 1.57 * iqr / np.sqrt(N)
1204
+
1205
+ return notch_min, notch_max
1206
+
1207
+ # output is a list of dicts
1208
+ bxpstats = []
1209
+
1210
+ # convert X to a list of lists
1211
+ X = _reshape_2D(X, "X")
1212
+
1213
+ ncols = len(X)
1214
+ if labels is None:
1215
+ labels = itertools.repeat(None)
1216
+ elif len(labels) != ncols:
1217
+ raise ValueError("Dimensions of labels and X must be compatible")
1218
+
1219
+ input_whis = whis
1220
+ for ii, (x, label) in enumerate(zip(X, labels)):
1221
+
1222
+ # empty dict
1223
+ stats = {}
1224
+ if label is not None:
1225
+ stats['label'] = label
1226
+
1227
+ # restore whis to the input values in case it got changed in the loop
1228
+ whis = input_whis
1229
+
1230
+ # note tricksiness, append up here and then mutate below
1231
+ bxpstats.append(stats)
1232
+
1233
+ # if empty, bail
1234
+ if len(x) == 0:
1235
+ stats['fliers'] = np.array([])
1236
+ stats['mean'] = np.nan
1237
+ stats['med'] = np.nan
1238
+ stats['q1'] = np.nan
1239
+ stats['q3'] = np.nan
1240
+ stats['iqr'] = np.nan
1241
+ stats['cilo'] = np.nan
1242
+ stats['cihi'] = np.nan
1243
+ stats['whislo'] = np.nan
1244
+ stats['whishi'] = np.nan
1245
+ continue
1246
+
1247
+ # up-convert to an array, just to be safe
1248
+ x = np.ma.asarray(x)
1249
+ x = x.data[~x.mask].ravel()
1250
+
1251
+ # arithmetic mean
1252
+ stats['mean'] = np.mean(x)
1253
+
1254
+ # medians and quartiles
1255
+ q1, med, q3 = np.percentile(x, [25, 50, 75])
1256
+
1257
+ # interquartile range
1258
+ stats['iqr'] = q3 - q1
1259
+ if stats['iqr'] == 0 and autorange:
1260
+ whis = (0, 100)
1261
+
1262
+ # conf. interval around median
1263
+ stats['cilo'], stats['cihi'] = _compute_conf_interval(
1264
+ x, med, stats['iqr'], bootstrap
1265
+ )
1266
+
1267
+ # lowest/highest non-outliers
1268
+ if np.iterable(whis) and not isinstance(whis, str):
1269
+ loval, hival = np.percentile(x, whis)
1270
+ elif np.isreal(whis):
1271
+ loval = q1 - whis * stats['iqr']
1272
+ hival = q3 + whis * stats['iqr']
1273
+ else:
1274
+ raise ValueError('whis must be a float or list of percentiles')
1275
+
1276
+ # get high extreme
1277
+ wiskhi = x[x <= hival]
1278
+ if len(wiskhi) == 0 or np.max(wiskhi) < q3:
1279
+ stats['whishi'] = q3
1280
+ else:
1281
+ stats['whishi'] = np.max(wiskhi)
1282
+
1283
+ # get low extreme
1284
+ wisklo = x[x >= loval]
1285
+ if len(wisklo) == 0 or np.min(wisklo) > q1:
1286
+ stats['whislo'] = q1
1287
+ else:
1288
+ stats['whislo'] = np.min(wisklo)
1289
+
1290
+ # compute a single array of outliers
1291
+ stats['fliers'] = np.concatenate([
1292
+ x[x < stats['whislo']],
1293
+ x[x > stats['whishi']],
1294
+ ])
1295
+
1296
+ # add in the remaining stats
1297
+ stats['q1'], stats['med'], stats['q3'] = q1, med, q3
1298
+
1299
+ return bxpstats
1300
+
1301
+
1302
+ #: Maps short codes for line style to their full name used by backends.
1303
+ ls_mapper = {'-': 'solid', '--': 'dashed', '-.': 'dashdot', ':': 'dotted'}
1304
+ #: Maps full names for line styles used by backends to their short codes.
1305
+ ls_mapper_r = {v: k for k, v in ls_mapper.items()}
1306
+
1307
+
1308
+ def contiguous_regions(mask):
1309
+ """
1310
+ Return a list of (ind0, ind1) such that ``mask[ind0:ind1].all()`` is
1311
+ True and we cover all such regions.
1312
+ """
1313
+ mask = np.asarray(mask, dtype=bool)
1314
+
1315
+ if not mask.size:
1316
+ return []
1317
+
1318
+ # Find the indices of region changes, and correct offset
1319
+ idx, = np.nonzero(mask[:-1] != mask[1:])
1320
+ idx += 1
1321
+
1322
+ # List operations are faster for moderately sized arrays
1323
+ idx = idx.tolist()
1324
+
1325
+ # Add first and/or last index if needed
1326
+ if mask[0]:
1327
+ idx = [0] + idx
1328
+ if mask[-1]:
1329
+ idx.append(len(mask))
1330
+
1331
+ return list(zip(idx[::2], idx[1::2]))
1332
+
1333
+
1334
+ def is_math_text(s):
1335
+ """
1336
+ Return whether the string *s* contains math expressions.
1337
+
1338
+ This is done by checking whether *s* contains an even number of
1339
+ non-escaped dollar signs.
1340
+ """
1341
+ s = str(s)
1342
+ dollar_count = s.count(r'$') - s.count(r'\$')
1343
+ even_dollars = (dollar_count > 0 and dollar_count % 2 == 0)
1344
+ return even_dollars
1345
+
1346
+
1347
+ def _to_unmasked_float_array(x):
1348
+ """
1349
+ Convert a sequence to a float array; if input was a masked array, masked
1350
+ values are converted to nans.
1351
+ """
1352
+ if hasattr(x, 'mask'):
1353
+ return np.ma.asarray(x, float).filled(np.nan)
1354
+ else:
1355
+ return np.asarray(x, float)
1356
+
1357
+
1358
+ def _check_1d(x):
1359
+ """Convert scalars to 1D arrays; pass-through arrays as is."""
1360
+ # Unpack in case of e.g. Pandas or xarray object
1361
+ x = _unpack_to_numpy(x)
1362
+ # plot requires `shape` and `ndim`. If passed an
1363
+ # object that doesn't provide them, then force to numpy array.
1364
+ # Note this will strip unit information.
1365
+ if (not hasattr(x, 'shape') or
1366
+ not hasattr(x, 'ndim') or
1367
+ len(x.shape) < 1):
1368
+ return np.atleast_1d(x)
1369
+ else:
1370
+ return x
1371
+
1372
+
1373
+ def _reshape_2D(X, name):
1374
+ """
1375
+ Use Fortran ordering to convert ndarrays and lists of iterables to lists of
1376
+ 1D arrays.
1377
+
1378
+ Lists of iterables are converted by applying `numpy.asanyarray` to each of
1379
+ their elements. 1D ndarrays are returned in a singleton list containing
1380
+ them. 2D ndarrays are converted to the list of their *columns*.
1381
+
1382
+ *name* is used to generate the error message for invalid inputs.
1383
+ """
1384
+
1385
+ # Unpack in case of e.g. Pandas or xarray object
1386
+ X = _unpack_to_numpy(X)
1387
+
1388
+ # Iterate over columns for ndarrays.
1389
+ if isinstance(X, np.ndarray):
1390
+ X = X.transpose()
1391
+
1392
+ if len(X) == 0:
1393
+ return [[]]
1394
+ elif X.ndim == 1 and np.ndim(X[0]) == 0:
1395
+ # 1D array of scalars: directly return it.
1396
+ return [X]
1397
+ elif X.ndim in [1, 2]:
1398
+ # 2D array, or 1D array of iterables: flatten them first.
1399
+ return [np.reshape(x, -1) for x in X]
1400
+ else:
1401
+ raise ValueError(f'{name} must have 2 or fewer dimensions')
1402
+
1403
+ # Iterate over list of iterables.
1404
+ if len(X) == 0:
1405
+ return [[]]
1406
+
1407
+ result = []
1408
+ is_1d = True
1409
+ for xi in X:
1410
+ # check if this is iterable, except for strings which we
1411
+ # treat as singletons.
1412
+ if not isinstance(xi, str):
1413
+ try:
1414
+ iter(xi)
1415
+ except TypeError:
1416
+ pass
1417
+ else:
1418
+ is_1d = False
1419
+ xi = np.asanyarray(xi)
1420
+ nd = np.ndim(xi)
1421
+ if nd > 1:
1422
+ raise ValueError(f'{name} must have 2 or fewer dimensions')
1423
+ result.append(xi.reshape(-1))
1424
+
1425
+ if is_1d:
1426
+ # 1D array of scalars: directly return it.
1427
+ return [np.reshape(result, -1)]
1428
+ else:
1429
+ # 2D array, or 1D array of iterables: use flattened version.
1430
+ return result
1431
+
1432
+
1433
+ def violin_stats(X, method, points=100, quantiles=None):
1434
+ """
1435
+ Return a list of dictionaries of data which can be used to draw a series
1436
+ of violin plots.
1437
+
1438
+ See the ``Returns`` section below to view the required keys of the
1439
+ dictionary.
1440
+
1441
+ Users can skip this function and pass a user-defined set of dictionaries
1442
+ with the same keys to `~.axes.Axes.violinplot` instead of using Matplotlib
1443
+ to do the calculations. See the *Returns* section below for the keys
1444
+ that must be present in the dictionaries.
1445
+
1446
+ Parameters
1447
+ ----------
1448
+ X : array-like
1449
+ Sample data that will be used to produce the gaussian kernel density
1450
+ estimates. Must have 2 or fewer dimensions.
1451
+
1452
+ method : callable
1453
+ The method used to calculate the kernel density estimate for each
1454
+ column of data. When called via ``method(v, coords)``, it should
1455
+ return a vector of the values of the KDE evaluated at the values
1456
+ specified in coords.
1457
+
1458
+ points : int, default: 100
1459
+ Defines the number of points to evaluate each of the gaussian kernel
1460
+ density estimates at.
1461
+
1462
+ quantiles : array-like, default: None
1463
+ Defines (if not None) a list of floats in interval [0, 1] for each
1464
+ column of data, which represents the quantiles that will be rendered
1465
+ for that column of data. Must have 2 or fewer dimensions. 1D array will
1466
+ be treated as a singleton list containing them.
1467
+
1468
+ Returns
1469
+ -------
1470
+ list of dict
1471
+ A list of dictionaries containing the results for each column of data.
1472
+ The dictionaries contain at least the following:
1473
+
1474
+ - coords: A list of scalars containing the coordinates this particular
1475
+ kernel density estimate was evaluated at.
1476
+ - vals: A list of scalars containing the values of the kernel density
1477
+ estimate at each of the coordinates given in *coords*.
1478
+ - mean: The mean value for this column of data.
1479
+ - median: The median value for this column of data.
1480
+ - min: The minimum value for this column of data.
1481
+ - max: The maximum value for this column of data.
1482
+ - quantiles: The quantile values for this column of data.
1483
+ """
1484
+
1485
+ # List of dictionaries describing each of the violins.
1486
+ vpstats = []
1487
+
1488
+ # Want X to be a list of data sequences
1489
+ X = _reshape_2D(X, "X")
1490
+
1491
+ # Want quantiles to be as the same shape as data sequences
1492
+ if quantiles is not None and len(quantiles) != 0:
1493
+ quantiles = _reshape_2D(quantiles, "quantiles")
1494
+ # Else, mock quantiles if it's none or empty
1495
+ else:
1496
+ quantiles = [[]] * len(X)
1497
+
1498
+ # quantiles should have the same size as dataset
1499
+ if len(X) != len(quantiles):
1500
+ raise ValueError("List of violinplot statistics and quantiles values"
1501
+ " must have the same length")
1502
+
1503
+ # Zip x and quantiles
1504
+ for (x, q) in zip(X, quantiles):
1505
+ # Dictionary of results for this distribution
1506
+ stats = {}
1507
+
1508
+ # Calculate basic stats for the distribution
1509
+ min_val = np.min(x)
1510
+ max_val = np.max(x)
1511
+ quantile_val = np.percentile(x, 100 * q)
1512
+
1513
+ # Evaluate the kernel density estimate
1514
+ coords = np.linspace(min_val, max_val, points)
1515
+ stats['vals'] = method(x, coords)
1516
+ stats['coords'] = coords
1517
+
1518
+ # Store additional statistics for this distribution
1519
+ stats['mean'] = np.mean(x)
1520
+ stats['median'] = np.median(x)
1521
+ stats['min'] = min_val
1522
+ stats['max'] = max_val
1523
+ stats['quantiles'] = np.atleast_1d(quantile_val)
1524
+
1525
+ # Append to output
1526
+ vpstats.append(stats)
1527
+
1528
+ return vpstats
1529
+
1530
+
1531
+ def pts_to_prestep(x, *args):
1532
+ """
1533
+ Convert continuous line to pre-steps.
1534
+
1535
+ Given a set of ``N`` points, convert to ``2N - 1`` points, which when
1536
+ connected linearly give a step function which changes values at the
1537
+ beginning of the intervals.
1538
+
1539
+ Parameters
1540
+ ----------
1541
+ x : array
1542
+ The x location of the steps. May be empty.
1543
+
1544
+ y1, ..., yp : array
1545
+ y arrays to be turned into steps; all must be the same length as ``x``.
1546
+
1547
+ Returns
1548
+ -------
1549
+ array
1550
+ The x and y values converted to steps in the same order as the input;
1551
+ can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1552
+ length ``N``, each of these arrays will be length ``2N + 1``. For
1553
+ ``N=0``, the length will be 0.
1554
+
1555
+ Examples
1556
+ --------
1557
+ >>> x_s, y1_s, y2_s = pts_to_prestep(x, y1, y2)
1558
+ """
1559
+ steps = np.zeros((1 + len(args), max(2 * len(x) - 1, 0)))
1560
+ # In all `pts_to_*step` functions, only assign once using *x* and *args*,
1561
+ # as converting to an array may be expensive.
1562
+ steps[0, 0::2] = x
1563
+ steps[0, 1::2] = steps[0, 0:-2:2]
1564
+ steps[1:, 0::2] = args
1565
+ steps[1:, 1::2] = steps[1:, 2::2]
1566
+ return steps
1567
+
1568
+
1569
+ def pts_to_poststep(x, *args):
1570
+ """
1571
+ Convert continuous line to post-steps.
1572
+
1573
+ Given a set of ``N`` points convert to ``2N + 1`` points, which when
1574
+ connected linearly give a step function which changes values at the end of
1575
+ the intervals.
1576
+
1577
+ Parameters
1578
+ ----------
1579
+ x : array
1580
+ The x location of the steps. May be empty.
1581
+
1582
+ y1, ..., yp : array
1583
+ y arrays to be turned into steps; all must be the same length as ``x``.
1584
+
1585
+ Returns
1586
+ -------
1587
+ array
1588
+ The x and y values converted to steps in the same order as the input;
1589
+ can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1590
+ length ``N``, each of these arrays will be length ``2N + 1``. For
1591
+ ``N=0``, the length will be 0.
1592
+
1593
+ Examples
1594
+ --------
1595
+ >>> x_s, y1_s, y2_s = pts_to_poststep(x, y1, y2)
1596
+ """
1597
+ steps = np.zeros((1 + len(args), max(2 * len(x) - 1, 0)))
1598
+ steps[0, 0::2] = x
1599
+ steps[0, 1::2] = steps[0, 2::2]
1600
+ steps[1:, 0::2] = args
1601
+ steps[1:, 1::2] = steps[1:, 0:-2:2]
1602
+ return steps
1603
+
1604
+
1605
+ def pts_to_midstep(x, *args):
1606
+ """
1607
+ Convert continuous line to mid-steps.
1608
+
1609
+ Given a set of ``N`` points convert to ``2N`` points which when connected
1610
+ linearly give a step function which changes values at the middle of the
1611
+ intervals.
1612
+
1613
+ Parameters
1614
+ ----------
1615
+ x : array
1616
+ The x location of the steps. May be empty.
1617
+
1618
+ y1, ..., yp : array
1619
+ y arrays to be turned into steps; all must be the same length as
1620
+ ``x``.
1621
+
1622
+ Returns
1623
+ -------
1624
+ array
1625
+ The x and y values converted to steps in the same order as the input;
1626
+ can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is
1627
+ length ``N``, each of these arrays will be length ``2N``.
1628
+
1629
+ Examples
1630
+ --------
1631
+ >>> x_s, y1_s, y2_s = pts_to_midstep(x, y1, y2)
1632
+ """
1633
+ steps = np.zeros((1 + len(args), 2 * len(x)))
1634
+ x = np.asanyarray(x)
1635
+ steps[0, 1:-1:2] = steps[0, 2::2] = (x[:-1] + x[1:]) / 2
1636
+ steps[0, :1] = x[:1] # Also works for zero-sized input.
1637
+ steps[0, -1:] = x[-1:]
1638
+ steps[1:, 0::2] = args
1639
+ steps[1:, 1::2] = steps[1:, 0::2]
1640
+ return steps
1641
+
1642
+
1643
+ STEP_LOOKUP_MAP = {'default': lambda x, y: (x, y),
1644
+ 'steps': pts_to_prestep,
1645
+ 'steps-pre': pts_to_prestep,
1646
+ 'steps-post': pts_to_poststep,
1647
+ 'steps-mid': pts_to_midstep}
1648
+
1649
+
1650
+ def index_of(y):
1651
+ """
1652
+ A helper function to create reasonable x values for the given *y*.
1653
+
1654
+ This is used for plotting (x, y) if x values are not explicitly given.
1655
+
1656
+ First try ``y.index`` (assuming *y* is a `pandas.Series`), if that
1657
+ fails, use ``range(len(y))``.
1658
+
1659
+ This will be extended in the future to deal with more types of
1660
+ labeled data.
1661
+
1662
+ Parameters
1663
+ ----------
1664
+ y : float or array-like
1665
+
1666
+ Returns
1667
+ -------
1668
+ x, y : ndarray
1669
+ The x and y values to plot.
1670
+ """
1671
+ try:
1672
+ return y.index.to_numpy(), y.to_numpy()
1673
+ except AttributeError:
1674
+ pass
1675
+ try:
1676
+ y = _check_1d(y)
1677
+ except (VisibleDeprecationWarning, ValueError):
1678
+ # NumPy 1.19 will warn on ragged input, and we can't actually use it.
1679
+ pass
1680
+ else:
1681
+ return np.arange(y.shape[0], dtype=float), y
1682
+ raise ValueError('Input could not be cast to an at-least-1D NumPy array')
1683
+
1684
+
1685
+ def safe_first_element(obj):
1686
+ """
1687
+ Return the first element in *obj*.
1688
+
1689
+ This is a type-independent way of obtaining the first element,
1690
+ supporting both index access and the iterator protocol.
1691
+ """
1692
+ if isinstance(obj, collections.abc.Iterator):
1693
+ # needed to accept `array.flat` as input.
1694
+ # np.flatiter reports as an instance of collections.Iterator but can still be
1695
+ # indexed via []. This has the side effect of re-setting the iterator, but
1696
+ # that is acceptable.
1697
+ try:
1698
+ return obj[0]
1699
+ except TypeError:
1700
+ pass
1701
+ raise RuntimeError("matplotlib does not support generators as input")
1702
+ return next(iter(obj))
1703
+
1704
+
1705
+ def _safe_first_finite(obj):
1706
+ """
1707
+ Return the first finite element in *obj* if one is available and skip_nonfinite is
1708
+ True. Otherwise, return the first element.
1709
+
1710
+ This is a method for internal use.
1711
+
1712
+ This is a type-independent way of obtaining the first finite element, supporting
1713
+ both index access and the iterator protocol.
1714
+ """
1715
+ def safe_isfinite(val):
1716
+ if val is None:
1717
+ return False
1718
+ try:
1719
+ return math.isfinite(val)
1720
+ except (TypeError, ValueError):
1721
+ # if the outer object is 2d, then val is a 1d array, and
1722
+ # - math.isfinite(numpy.zeros(3)) raises TypeError
1723
+ # - math.isfinite(torch.zeros(3)) raises ValueError
1724
+ pass
1725
+ try:
1726
+ return np.isfinite(val) if np.isscalar(val) else True
1727
+ except TypeError:
1728
+ # This is something that NumPy cannot make heads or tails of,
1729
+ # assume "finite"
1730
+ return True
1731
+
1732
+ if isinstance(obj, np.flatiter):
1733
+ # TODO do the finite filtering on this
1734
+ return obj[0]
1735
+ elif isinstance(obj, collections.abc.Iterator):
1736
+ raise RuntimeError("matplotlib does not support generators as input")
1737
+ else:
1738
+ for val in obj:
1739
+ if safe_isfinite(val):
1740
+ return val
1741
+ return safe_first_element(obj)
1742
+
1743
+
1744
+ def sanitize_sequence(data):
1745
+ """
1746
+ Convert dictview objects to list. Other inputs are returned unchanged.
1747
+ """
1748
+ return (list(data) if isinstance(data, collections.abc.MappingView)
1749
+ else data)
1750
+
1751
+
1752
+ def normalize_kwargs(kw, alias_mapping=None):
1753
+ """
1754
+ Helper function to normalize kwarg inputs.
1755
+
1756
+ Parameters
1757
+ ----------
1758
+ kw : dict or None
1759
+ A dict of keyword arguments. None is explicitly supported and treated
1760
+ as an empty dict, to support functions with an optional parameter of
1761
+ the form ``props=None``.
1762
+
1763
+ alias_mapping : dict or Artist subclass or Artist instance, optional
1764
+ A mapping between a canonical name to a list of aliases, in order of
1765
+ precedence from lowest to highest.
1766
+
1767
+ If the canonical value is not in the list it is assumed to have the
1768
+ highest priority.
1769
+
1770
+ If an Artist subclass or instance is passed, use its properties alias
1771
+ mapping.
1772
+
1773
+ Raises
1774
+ ------
1775
+ TypeError
1776
+ To match what Python raises if invalid arguments/keyword arguments are
1777
+ passed to a callable.
1778
+ """
1779
+ from matplotlib.artist import Artist
1780
+
1781
+ if kw is None:
1782
+ return {}
1783
+
1784
+ # deal with default value of alias_mapping
1785
+ if alias_mapping is None:
1786
+ alias_mapping = {}
1787
+ elif (isinstance(alias_mapping, type) and issubclass(alias_mapping, Artist)
1788
+ or isinstance(alias_mapping, Artist)):
1789
+ alias_mapping = getattr(alias_mapping, "_alias_map", {})
1790
+
1791
+ to_canonical = {alias: canonical
1792
+ for canonical, alias_list in alias_mapping.items()
1793
+ for alias in alias_list}
1794
+ canonical_to_seen = {}
1795
+ ret = {} # output dictionary
1796
+
1797
+ for k, v in kw.items():
1798
+ canonical = to_canonical.get(k, k)
1799
+ if canonical in canonical_to_seen:
1800
+ raise TypeError(f"Got both {canonical_to_seen[canonical]!r} and "
1801
+ f"{k!r}, which are aliases of one another")
1802
+ canonical_to_seen[canonical] = k
1803
+ ret[canonical] = v
1804
+
1805
+ return ret
1806
+
1807
+
1808
+ @contextlib.contextmanager
1809
+ def _lock_path(path):
1810
+ """
1811
+ Context manager for locking a path.
1812
+
1813
+ Usage::
1814
+
1815
+ with _lock_path(path):
1816
+ ...
1817
+
1818
+ Another thread or process that attempts to lock the same path will wait
1819
+ until this context manager is exited.
1820
+
1821
+ The lock is implemented by creating a temporary file in the parent
1822
+ directory, so that directory must exist and be writable.
1823
+ """
1824
+ path = Path(path)
1825
+ lock_path = path.with_name(path.name + ".matplotlib-lock")
1826
+ retries = 50
1827
+ sleeptime = 0.1
1828
+ for _ in range(retries):
1829
+ try:
1830
+ with lock_path.open("xb"):
1831
+ break
1832
+ except FileExistsError:
1833
+ time.sleep(sleeptime)
1834
+ else:
1835
+ raise TimeoutError("""\
1836
+ Lock error: Matplotlib failed to acquire the following lock file:
1837
+ {}
1838
+ This maybe due to another process holding this lock file. If you are sure no
1839
+ other Matplotlib process is running, remove this file and try again.""".format(
1840
+ lock_path))
1841
+ try:
1842
+ yield
1843
+ finally:
1844
+ lock_path.unlink()
1845
+
1846
+
1847
+ def _topmost_artist(
1848
+ artists,
1849
+ _cached_max=functools.partial(max, key=operator.attrgetter("zorder"))):
1850
+ """
1851
+ Get the topmost artist of a list.
1852
+
1853
+ In case of a tie, return the *last* of the tied artists, as it will be
1854
+ drawn on top of the others. `max` returns the first maximum in case of
1855
+ ties, so we need to iterate over the list in reverse order.
1856
+ """
1857
+ return _cached_max(reversed(artists))
1858
+
1859
+
1860
+ def _str_equal(obj, s):
1861
+ """
1862
+ Return whether *obj* is a string equal to string *s*.
1863
+
1864
+ This helper solely exists to handle the case where *obj* is a numpy array,
1865
+ because in such cases, a naive ``obj == s`` would yield an array, which
1866
+ cannot be used in a boolean context.
1867
+ """
1868
+ return isinstance(obj, str) and obj == s
1869
+
1870
+
1871
+ def _str_lower_equal(obj, s):
1872
+ """
1873
+ Return whether *obj* is a string equal, when lowercased, to string *s*.
1874
+
1875
+ This helper solely exists to handle the case where *obj* is a numpy array,
1876
+ because in such cases, a naive ``obj == s`` would yield an array, which
1877
+ cannot be used in a boolean context.
1878
+ """
1879
+ return isinstance(obj, str) and obj.lower() == s
1880
+
1881
+
1882
+ def _array_perimeter(arr):
1883
+ """
1884
+ Get the elements on the perimeter of *arr*.
1885
+
1886
+ Parameters
1887
+ ----------
1888
+ arr : ndarray, shape (M, N)
1889
+ The input array.
1890
+
1891
+ Returns
1892
+ -------
1893
+ ndarray, shape (2*(M - 1) + 2*(N - 1),)
1894
+ The elements on the perimeter of the array::
1895
+
1896
+ [arr[0, 0], ..., arr[0, -1], ..., arr[-1, -1], ..., arr[-1, 0], ...]
1897
+
1898
+ Examples
1899
+ --------
1900
+ >>> i, j = np.ogrid[:3, :4]
1901
+ >>> a = i*10 + j
1902
+ >>> a
1903
+ array([[ 0, 1, 2, 3],
1904
+ [10, 11, 12, 13],
1905
+ [20, 21, 22, 23]])
1906
+ >>> _array_perimeter(a)
1907
+ array([ 0, 1, 2, 3, 13, 23, 22, 21, 20, 10])
1908
+ """
1909
+ # note we use Python's half-open ranges to avoid repeating
1910
+ # the corners
1911
+ forward = np.s_[0:-1] # [0 ... -1)
1912
+ backward = np.s_[-1:0:-1] # [-1 ... 0)
1913
+ return np.concatenate((
1914
+ arr[0, forward],
1915
+ arr[forward, -1],
1916
+ arr[-1, backward],
1917
+ arr[backward, 0],
1918
+ ))
1919
+
1920
+
1921
+ def _unfold(arr, axis, size, step):
1922
+ """
1923
+ Append an extra dimension containing sliding windows along *axis*.
1924
+
1925
+ All windows are of size *size* and begin with every *step* elements.
1926
+
1927
+ Parameters
1928
+ ----------
1929
+ arr : ndarray, shape (N_1, ..., N_k)
1930
+ The input array
1931
+ axis : int
1932
+ Axis along which the windows are extracted
1933
+ size : int
1934
+ Size of the windows
1935
+ step : int
1936
+ Stride between first elements of subsequent windows.
1937
+
1938
+ Returns
1939
+ -------
1940
+ ndarray, shape (N_1, ..., 1 + (N_axis-size)/step, ..., N_k, size)
1941
+
1942
+ Examples
1943
+ --------
1944
+ >>> i, j = np.ogrid[:3, :7]
1945
+ >>> a = i*10 + j
1946
+ >>> a
1947
+ array([[ 0, 1, 2, 3, 4, 5, 6],
1948
+ [10, 11, 12, 13, 14, 15, 16],
1949
+ [20, 21, 22, 23, 24, 25, 26]])
1950
+ >>> _unfold(a, axis=1, size=3, step=2)
1951
+ array([[[ 0, 1, 2],
1952
+ [ 2, 3, 4],
1953
+ [ 4, 5, 6]],
1954
+ [[10, 11, 12],
1955
+ [12, 13, 14],
1956
+ [14, 15, 16]],
1957
+ [[20, 21, 22],
1958
+ [22, 23, 24],
1959
+ [24, 25, 26]]])
1960
+ """
1961
+ new_shape = [*arr.shape, size]
1962
+ new_strides = [*arr.strides, arr.strides[axis]]
1963
+ new_shape[axis] = (new_shape[axis] - size) // step + 1
1964
+ new_strides[axis] = new_strides[axis] * step
1965
+ return np.lib.stride_tricks.as_strided(arr,
1966
+ shape=new_shape,
1967
+ strides=new_strides,
1968
+ writeable=False)
1969
+
1970
+
1971
+ def _array_patch_perimeters(x, rstride, cstride):
1972
+ """
1973
+ Extract perimeters of patches from *arr*.
1974
+
1975
+ Extracted patches are of size (*rstride* + 1) x (*cstride* + 1) and
1976
+ share perimeters with their neighbors. The ordering of the vertices matches
1977
+ that returned by ``_array_perimeter``.
1978
+
1979
+ Parameters
1980
+ ----------
1981
+ x : ndarray, shape (N, M)
1982
+ Input array
1983
+ rstride : int
1984
+ Vertical (row) stride between corresponding elements of each patch
1985
+ cstride : int
1986
+ Horizontal (column) stride between corresponding elements of each patch
1987
+
1988
+ Returns
1989
+ -------
1990
+ ndarray, shape (N/rstride * M/cstride, 2 * (rstride + cstride))
1991
+ """
1992
+ assert rstride > 0 and cstride > 0
1993
+ assert (x.shape[0] - 1) % rstride == 0
1994
+ assert (x.shape[1] - 1) % cstride == 0
1995
+ # We build up each perimeter from four half-open intervals. Here is an
1996
+ # illustrated explanation for rstride == cstride == 3
1997
+ #
1998
+ # T T T R
1999
+ # L R
2000
+ # L R
2001
+ # L B B B
2002
+ #
2003
+ # where T means that this element will be in the top array, R for right,
2004
+ # B for bottom and L for left. Each of the arrays below has a shape of:
2005
+ #
2006
+ # (number of perimeters that can be extracted vertically,
2007
+ # number of perimeters that can be extracted horizontally,
2008
+ # cstride for top and bottom and rstride for left and right)
2009
+ #
2010
+ # Note that _unfold doesn't incur any memory copies, so the only costly
2011
+ # operation here is the np.concatenate.
2012
+ top = _unfold(x[:-1:rstride, :-1], 1, cstride, cstride)
2013
+ bottom = _unfold(x[rstride::rstride, 1:], 1, cstride, cstride)[..., ::-1]
2014
+ right = _unfold(x[:-1, cstride::cstride], 0, rstride, rstride)
2015
+ left = _unfold(x[1:, :-1:cstride], 0, rstride, rstride)[..., ::-1]
2016
+ return (np.concatenate((top, right, bottom, left), axis=2)
2017
+ .reshape(-1, 2 * (rstride + cstride)))
2018
+
2019
+
2020
+ @contextlib.contextmanager
2021
+ def _setattr_cm(obj, **kwargs):
2022
+ """
2023
+ Temporarily set some attributes; restore original state at context exit.
2024
+ """
2025
+ sentinel = object()
2026
+ origs = {}
2027
+ for attr in kwargs:
2028
+ orig = getattr(obj, attr, sentinel)
2029
+ if attr in obj.__dict__ or orig is sentinel:
2030
+ # if we are pulling from the instance dict or the object
2031
+ # does not have this attribute we can trust the above
2032
+ origs[attr] = orig
2033
+ else:
2034
+ # if the attribute is not in the instance dict it must be
2035
+ # from the class level
2036
+ cls_orig = getattr(type(obj), attr)
2037
+ # if we are dealing with a property (but not a general descriptor)
2038
+ # we want to set the original value back.
2039
+ if isinstance(cls_orig, property):
2040
+ origs[attr] = orig
2041
+ # otherwise this is _something_ we are going to shadow at
2042
+ # the instance dict level from higher up in the MRO. We
2043
+ # are going to assume we can delattr(obj, attr) to clean
2044
+ # up after ourselves. It is possible that this code will
2045
+ # fail if used with a non-property custom descriptor which
2046
+ # implements __set__ (and __delete__ does not act like a
2047
+ # stack). However, this is an internal tool and we do not
2048
+ # currently have any custom descriptors.
2049
+ else:
2050
+ origs[attr] = sentinel
2051
+
2052
+ try:
2053
+ for attr, val in kwargs.items():
2054
+ setattr(obj, attr, val)
2055
+ yield
2056
+ finally:
2057
+ for attr, orig in origs.items():
2058
+ if orig is sentinel:
2059
+ delattr(obj, attr)
2060
+ else:
2061
+ setattr(obj, attr, orig)
2062
+
2063
+
2064
+ class _OrderedSet(collections.abc.MutableSet):
2065
+ def __init__(self):
2066
+ self._od = collections.OrderedDict()
2067
+
2068
+ def __contains__(self, key):
2069
+ return key in self._od
2070
+
2071
+ def __iter__(self):
2072
+ return iter(self._od)
2073
+
2074
+ def __len__(self):
2075
+ return len(self._od)
2076
+
2077
+ def add(self, key):
2078
+ self._od.pop(key, None)
2079
+ self._od[key] = None
2080
+
2081
+ def discard(self, key):
2082
+ self._od.pop(key, None)
2083
+
2084
+
2085
+ # Agg's buffers are unmultiplied RGBA8888, which neither PyQt<=5.1 nor cairo
2086
+ # support; however, both do support premultiplied ARGB32.
2087
+
2088
+
2089
+ def _premultiplied_argb32_to_unmultiplied_rgba8888(buf):
2090
+ """
2091
+ Convert a premultiplied ARGB32 buffer to an unmultiplied RGBA8888 buffer.
2092
+ """
2093
+ rgba = np.take( # .take() ensures C-contiguity of the result.
2094
+ buf,
2095
+ [2, 1, 0, 3] if sys.byteorder == "little" else [1, 2, 3, 0], axis=2)
2096
+ rgb = rgba[..., :-1]
2097
+ alpha = rgba[..., -1]
2098
+ # Un-premultiply alpha. The formula is the same as in cairo-png.c.
2099
+ mask = alpha != 0
2100
+ for channel in np.rollaxis(rgb, -1):
2101
+ channel[mask] = (
2102
+ (channel[mask].astype(int) * 255 + alpha[mask] // 2)
2103
+ // alpha[mask])
2104
+ return rgba
2105
+
2106
+
2107
+ def _unmultiplied_rgba8888_to_premultiplied_argb32(rgba8888):
2108
+ """
2109
+ Convert an unmultiplied RGBA8888 buffer to a premultiplied ARGB32 buffer.
2110
+ """
2111
+ if sys.byteorder == "little":
2112
+ argb32 = np.take(rgba8888, [2, 1, 0, 3], axis=2)
2113
+ rgb24 = argb32[..., :-1]
2114
+ alpha8 = argb32[..., -1:]
2115
+ else:
2116
+ argb32 = np.take(rgba8888, [3, 0, 1, 2], axis=2)
2117
+ alpha8 = argb32[..., :1]
2118
+ rgb24 = argb32[..., 1:]
2119
+ # Only bother premultiplying when the alpha channel is not fully opaque,
2120
+ # as the cost is not negligible. The unsafe cast is needed to do the
2121
+ # multiplication in-place in an integer buffer.
2122
+ if alpha8.min() != 0xff:
2123
+ np.multiply(rgb24, alpha8 / 0xff, out=rgb24, casting="unsafe")
2124
+ return argb32
2125
+
2126
+
2127
+ def _get_nonzero_slices(buf):
2128
+ """
2129
+ Return the bounds of the nonzero region of a 2D array as a pair of slices.
2130
+
2131
+ ``buf[_get_nonzero_slices(buf)]`` is the smallest sub-rectangle in *buf*
2132
+ that encloses all non-zero entries in *buf*. If *buf* is fully zero, then
2133
+ ``(slice(0, 0), slice(0, 0))`` is returned.
2134
+ """
2135
+ x_nz, = buf.any(axis=0).nonzero()
2136
+ y_nz, = buf.any(axis=1).nonzero()
2137
+ if len(x_nz) and len(y_nz):
2138
+ l, r = x_nz[[0, -1]]
2139
+ b, t = y_nz[[0, -1]]
2140
+ return slice(b, t + 1), slice(l, r + 1)
2141
+ else:
2142
+ return slice(0, 0), slice(0, 0)
2143
+
2144
+
2145
+ def _pformat_subprocess(command):
2146
+ """Pretty-format a subprocess command for printing/logging purposes."""
2147
+ return (command if isinstance(command, str)
2148
+ else " ".join(shlex.quote(os.fspath(arg)) for arg in command))
2149
+
2150
+
2151
+ def _check_and_log_subprocess(command, logger, **kwargs):
2152
+ """
2153
+ Run *command*, returning its stdout output if it succeeds.
2154
+
2155
+ If it fails (exits with nonzero return code), raise an exception whose text
2156
+ includes the failed command and captured stdout and stderr output.
2157
+
2158
+ Regardless of the return code, the command is logged at DEBUG level on
2159
+ *logger*. In case of success, the output is likewise logged.
2160
+ """
2161
+ logger.debug('%s', _pformat_subprocess(command))
2162
+ proc = subprocess.run(command, capture_output=True, **kwargs)
2163
+ if proc.returncode:
2164
+ stdout = proc.stdout
2165
+ if isinstance(stdout, bytes):
2166
+ stdout = stdout.decode()
2167
+ stderr = proc.stderr
2168
+ if isinstance(stderr, bytes):
2169
+ stderr = stderr.decode()
2170
+ raise RuntimeError(
2171
+ f"The command\n"
2172
+ f" {_pformat_subprocess(command)}\n"
2173
+ f"failed and generated the following output:\n"
2174
+ f"{stdout}\n"
2175
+ f"and the following error:\n"
2176
+ f"{stderr}")
2177
+ if proc.stdout:
2178
+ logger.debug("stdout:\n%s", proc.stdout)
2179
+ if proc.stderr:
2180
+ logger.debug("stderr:\n%s", proc.stderr)
2181
+ return proc.stdout
2182
+
2183
+
2184
+ def _setup_new_guiapp():
2185
+ """
2186
+ Perform OS-dependent setup when Matplotlib creates a new GUI application.
2187
+ """
2188
+ # Windows: If not explicit app user model id has been set yet (so we're not
2189
+ # already embedded), then set it to "matplotlib", so that taskbar icons are
2190
+ # correct.
2191
+ try:
2192
+ _c_internal_utils.Win32_GetCurrentProcessExplicitAppUserModelID()
2193
+ except OSError:
2194
+ _c_internal_utils.Win32_SetCurrentProcessExplicitAppUserModelID(
2195
+ "matplotlib")
2196
+
2197
+
2198
+ def _format_approx(number, precision):
2199
+ """
2200
+ Format the number with at most the number of decimals given as precision.
2201
+ Remove trailing zeros and possibly the decimal point.
2202
+ """
2203
+ return f'{number:.{precision}f}'.rstrip('0').rstrip('.') or '0'
2204
+
2205
+
2206
+ def _g_sig_digits(value, delta):
2207
+ """
2208
+ Return the number of significant digits to %g-format *value*, assuming that
2209
+ it is known with an error of *delta*.
2210
+ """
2211
+ if delta == 0:
2212
+ if value == 0:
2213
+ # if both value and delta are 0, np.spacing below returns 5e-324
2214
+ # which results in rather silly results
2215
+ return 3
2216
+ # delta = 0 may occur when trying to format values over a tiny range;
2217
+ # in that case, replace it by the distance to the closest float.
2218
+ delta = abs(np.spacing(value))
2219
+ # If e.g. value = 45.67 and delta = 0.02, then we want to round to 2 digits
2220
+ # after the decimal point (floor(log10(0.02)) = -2); 45.67 contributes 2
2221
+ # digits before the decimal point (floor(log10(45.67)) + 1 = 2): the total
2222
+ # is 4 significant digits. A value of 0 contributes 1 "digit" before the
2223
+ # decimal point.
2224
+ # For inf or nan, the precision doesn't matter.
2225
+ return max(
2226
+ 0,
2227
+ (math.floor(math.log10(abs(value))) + 1 if value else 1)
2228
+ - math.floor(math.log10(delta))) if math.isfinite(value) else 0
2229
+
2230
+
2231
+ def _unikey_or_keysym_to_mplkey(unikey, keysym):
2232
+ """
2233
+ Convert a Unicode key or X keysym to a Matplotlib key name.
2234
+
2235
+ The Unicode key is checked first; this avoids having to list most printable
2236
+ keysyms such as ``EuroSign``.
2237
+ """
2238
+ # For non-printable characters, gtk3 passes "\0" whereas tk passes an "".
2239
+ if unikey and unikey.isprintable():
2240
+ return unikey
2241
+ key = keysym.lower()
2242
+ if key.startswith("kp_"): # keypad_x (including kp_enter).
2243
+ key = key[3:]
2244
+ if key.startswith("page_"): # page_{up,down}
2245
+ key = key.replace("page_", "page")
2246
+ if key.endswith(("_l", "_r")): # alt_l, ctrl_l, shift_l.
2247
+ key = key[:-2]
2248
+ if sys.platform == "darwin" and key == "meta":
2249
+ # meta should be reported as command on mac
2250
+ key = "cmd"
2251
+ key = {
2252
+ "return": "enter",
2253
+ "prior": "pageup", # Used by tk.
2254
+ "next": "pagedown", # Used by tk.
2255
+ }.get(key, key)
2256
+ return key
2257
+
2258
+
2259
+ @functools.cache
2260
+ def _make_class_factory(mixin_class, fmt, attr_name=None):
2261
+ """
2262
+ Return a function that creates picklable classes inheriting from a mixin.
2263
+
2264
+ After ::
2265
+
2266
+ factory = _make_class_factory(FooMixin, fmt, attr_name)
2267
+ FooAxes = factory(Axes)
2268
+
2269
+ ``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is
2270
+ picklable** (picklability is what differentiates this from a plain call to
2271
+ `type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the
2272
+ base class is stored in the ``attr_name`` attribute, if not None.
2273
+
2274
+ Moreover, the return value of ``factory`` is memoized: calls with the same
2275
+ ``Axes`` class always return the same subclass.
2276
+ """
2277
+
2278
+ @functools.cache
2279
+ def class_factory(axes_class):
2280
+ # if we have already wrapped this class, declare victory!
2281
+ if issubclass(axes_class, mixin_class):
2282
+ return axes_class
2283
+
2284
+ # The parameter is named "axes_class" for backcompat but is really just
2285
+ # a base class; no axes semantics are used.
2286
+ base_class = axes_class
2287
+
2288
+ class subcls(mixin_class, base_class):
2289
+ # Better approximation than __module__ = "matplotlib.cbook".
2290
+ __module__ = mixin_class.__module__
2291
+
2292
+ def __reduce__(self):
2293
+ return (_picklable_class_constructor,
2294
+ (mixin_class, fmt, attr_name, base_class),
2295
+ self.__getstate__())
2296
+
2297
+ subcls.__name__ = subcls.__qualname__ = fmt.format(base_class.__name__)
2298
+ if attr_name is not None:
2299
+ setattr(subcls, attr_name, base_class)
2300
+ return subcls
2301
+
2302
+ class_factory.__module__ = mixin_class.__module__
2303
+ return class_factory
2304
+
2305
+
2306
+ def _picklable_class_constructor(mixin_class, fmt, attr_name, base_class):
2307
+ """Internal helper for _make_class_factory."""
2308
+ factory = _make_class_factory(mixin_class, fmt, attr_name)
2309
+ cls = factory(base_class)
2310
+ return cls.__new__(cls)
2311
+
2312
+
2313
+ def _is_torch_array(x):
2314
+ """Check if 'x' is a PyTorch Tensor."""
2315
+ try:
2316
+ # we're intentionally not attempting to import torch. If somebody
2317
+ # has created a torch array, torch should already be in sys.modules
2318
+ return isinstance(x, sys.modules['torch'].Tensor)
2319
+ except Exception: # TypeError, KeyError, AttributeError, maybe others?
2320
+ # we're attempting to access attributes on imported modules which
2321
+ # may have arbitrary user code, so we deliberately catch all exceptions
2322
+ return False
2323
+
2324
+
2325
+ def _is_jax_array(x):
2326
+ """Check if 'x' is a JAX Array."""
2327
+ try:
2328
+ # we're intentionally not attempting to import jax. If somebody
2329
+ # has created a jax array, jax should already be in sys.modules
2330
+ return isinstance(x, sys.modules['jax'].Array)
2331
+ except Exception: # TypeError, KeyError, AttributeError, maybe others?
2332
+ # we're attempting to access attributes on imported modules which
2333
+ # may have arbitrary user code, so we deliberately catch all exceptions
2334
+ return False
2335
+
2336
+
2337
+ def _is_tensorflow_array(x):
2338
+ """Check if 'x' is a TensorFlow Tensor or Variable."""
2339
+ try:
2340
+ # we're intentionally not attempting to import TensorFlow. If somebody
2341
+ # has created a TensorFlow array, TensorFlow should already be in sys.modules
2342
+ # we use `is_tensor` to not depend on the class structure of TensorFlow
2343
+ # arrays, as `tf.Variables` are not instances of `tf.Tensor`
2344
+ # (they both convert the same way)
2345
+ return isinstance(x, sys.modules['tensorflow'].is_tensor(x))
2346
+ except Exception: # TypeError, KeyError, AttributeError, maybe others?
2347
+ # we're attempting to access attributes on imported modules which
2348
+ # may have arbitrary user code, so we deliberately catch all exceptions
2349
+ return False
2350
+
2351
+
2352
+ def _unpack_to_numpy(x):
2353
+ """Internal helper to extract data from e.g. pandas and xarray objects."""
2354
+ if isinstance(x, np.ndarray):
2355
+ # If numpy, return directly
2356
+ return x
2357
+ if hasattr(x, 'to_numpy'):
2358
+ # Assume that any to_numpy() method actually returns a numpy array
2359
+ return x.to_numpy()
2360
+ if hasattr(x, 'values'):
2361
+ xtmp = x.values
2362
+ # For example a dict has a 'values' attribute, but it is not a property
2363
+ # so in this case we do not want to return a function
2364
+ if isinstance(xtmp, np.ndarray):
2365
+ return xtmp
2366
+ if _is_torch_array(x) or _is_jax_array(x) or _is_tensorflow_array(x):
2367
+ # using np.asarray() instead of explicitly __array__(), as the latter is
2368
+ # only _one_ of many methods, and it's the last resort, see also
2369
+ # https://numpy.org/devdocs/user/basics.interoperability.html#using-arbitrary-objects-in-numpy
2370
+ # therefore, let arrays do better if they can
2371
+ xtmp = np.asarray(x)
2372
+
2373
+ # In case np.asarray method does not return a numpy array in future
2374
+ if isinstance(xtmp, np.ndarray):
2375
+ return xtmp
2376
+ return x
2377
+
2378
+
2379
+ def _auto_format_str(fmt, value):
2380
+ """
2381
+ Apply *value* to the format string *fmt*.
2382
+
2383
+ This works both with unnamed %-style formatting and
2384
+ unnamed {}-style formatting. %-style formatting has priority.
2385
+ If *fmt* is %-style formattable that will be used. Otherwise,
2386
+ {}-formatting is applied. Strings without formatting placeholders
2387
+ are passed through as is.
2388
+
2389
+ Examples
2390
+ --------
2391
+ >>> _auto_format_str('%.2f m', 0.2)
2392
+ '0.20 m'
2393
+ >>> _auto_format_str('{} m', 0.2)
2394
+ '0.2 m'
2395
+ >>> _auto_format_str('const', 0.2)
2396
+ 'const'
2397
+ >>> _auto_format_str('%d or {}', 0.2)
2398
+ '0 or {}'
2399
+ """
2400
+ try:
2401
+ return fmt % (value,)
2402
+ except (TypeError, ValueError):
2403
+ return fmt.format(value)
2404
+
2405
+
2406
+ def _is_pandas_dataframe(x):
2407
+ """Check if 'x' is a Pandas DataFrame."""
2408
+ try:
2409
+ # we're intentionally not attempting to import Pandas. If somebody
2410
+ # has created a Pandas DataFrame, Pandas should already be in sys.modules
2411
+ return isinstance(x, sys.modules['pandas'].DataFrame)
2412
+ except Exception: # TypeError, KeyError, AttributeError, maybe others?
2413
+ # we're attempting to access attributes on imported modules which
2414
+ # may have arbitrary user code, so we deliberately catch all exceptions
2415
+ return False
lib/python3.12/site-packages/matplotlib/dviread.py ADDED
@@ -0,0 +1,1139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ A module for reading dvi files output by TeX. Several limitations make
3
+ this not (currently) useful as a general-purpose dvi preprocessor, but
4
+ it is currently used by the pdf backend for processing usetex text.
5
+
6
+ Interface::
7
+
8
+ with Dvi(filename, 72) as dvi:
9
+ # iterate over pages:
10
+ for page in dvi:
11
+ w, h, d = page.width, page.height, page.descent
12
+ for x, y, font, glyph, width in page.text:
13
+ fontname = font.texname
14
+ pointsize = font.size
15
+ ...
16
+ for x, y, height, width in page.boxes:
17
+ ...
18
+ """
19
+
20
+ from collections import namedtuple
21
+ import enum
22
+ from functools import lru_cache, partial, wraps
23
+ import logging
24
+ import os
25
+ from pathlib import Path
26
+ import re
27
+ import struct
28
+ import subprocess
29
+ import sys
30
+
31
+ import numpy as np
32
+
33
+ from matplotlib import _api, cbook
34
+
35
+ _log = logging.getLogger(__name__)
36
+
37
+ # Many dvi related files are looked for by external processes, require
38
+ # additional parsing, and are used many times per rendering, which is why they
39
+ # are cached using lru_cache().
40
+
41
+ # Dvi is a bytecode format documented in
42
+ # https://ctan.org/pkg/dvitype
43
+ # https://texdoc.org/serve/dvitype.pdf/0
44
+ #
45
+ # The file consists of a preamble, some number of pages, a postamble,
46
+ # and a finale. Different opcodes are allowed in different contexts,
47
+ # so the Dvi object has a parser state:
48
+ #
49
+ # pre: expecting the preamble
50
+ # outer: between pages (followed by a page or the postamble,
51
+ # also e.g. font definitions are allowed)
52
+ # page: processing a page
53
+ # post_post: state after the postamble (our current implementation
54
+ # just stops reading)
55
+ # finale: the finale (unimplemented in our current implementation)
56
+
57
+ _dvistate = enum.Enum('DviState', 'pre outer inpage post_post finale')
58
+
59
+ # The marks on a page consist of text and boxes. A page also has dimensions.
60
+ Page = namedtuple('Page', 'text boxes height width descent')
61
+ Box = namedtuple('Box', 'x y height width')
62
+
63
+
64
+ # Also a namedtuple, for backcompat.
65
+ class Text(namedtuple('Text', 'x y font glyph width')):
66
+ """
67
+ A glyph in the dvi file.
68
+
69
+ The *x* and *y* attributes directly position the glyph. The *font*,
70
+ *glyph*, and *width* attributes are kept public for back-compatibility,
71
+ but users wanting to draw the glyph themselves are encouraged to instead
72
+ load the font specified by `font_path` at `font_size`, warp it with the
73
+ effects specified by `font_effects`, and load the glyph specified by
74
+ `glyph_name_or_index`.
75
+ """
76
+
77
+ def _get_pdftexmap_entry(self):
78
+ return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname]
79
+
80
+ @property
81
+ def font_path(self):
82
+ """The `~pathlib.Path` to the font for this glyph."""
83
+ psfont = self._get_pdftexmap_entry()
84
+ if psfont.filename is None:
85
+ raise ValueError("No usable font file found for {} ({}); "
86
+ "the font may lack a Type-1 version"
87
+ .format(psfont.psname.decode("ascii"),
88
+ psfont.texname.decode("ascii")))
89
+ return Path(psfont.filename)
90
+
91
+ @property
92
+ def font_size(self):
93
+ """The font size."""
94
+ return self.font.size
95
+
96
+ @property
97
+ def font_effects(self):
98
+ """
99
+ The "font effects" dict for this glyph.
100
+
101
+ This dict contains the values for this glyph of SlantFont and
102
+ ExtendFont (if any), read off :file:`pdftex.map`.
103
+ """
104
+ return self._get_pdftexmap_entry().effects
105
+
106
+ @property
107
+ def glyph_name_or_index(self):
108
+ """
109
+ Either the glyph name or the native charmap glyph index.
110
+
111
+ If :file:`pdftex.map` specifies an encoding for this glyph's font, that
112
+ is a mapping of glyph indices to Adobe glyph names; use it to convert
113
+ dvi indices to glyph names. Callers can then convert glyph names to
114
+ glyph indices (with FT_Get_Name_Index/get_name_index), and load the
115
+ glyph using FT_Load_Glyph/load_glyph.
116
+
117
+ If :file:`pdftex.map` specifies no encoding, the indices directly map
118
+ to the font's "native" charmap; glyphs should directly load using
119
+ FT_Load_Char/load_char after selecting the native charmap.
120
+ """
121
+ entry = self._get_pdftexmap_entry()
122
+ return (_parse_enc(entry.encoding)[self.glyph]
123
+ if entry.encoding is not None else self.glyph)
124
+
125
+
126
+ # Opcode argument parsing
127
+ #
128
+ # Each of the following functions takes a Dvi object and delta, which is the
129
+ # difference between the opcode and the minimum opcode with the same meaning.
130
+ # Dvi opcodes often encode the number of argument bytes in this delta.
131
+ _arg_mapping = dict(
132
+ # raw: Return delta as is.
133
+ raw=lambda dvi, delta: delta,
134
+ # u1: Read 1 byte as an unsigned number.
135
+ u1=lambda dvi, delta: dvi._read_arg(1, signed=False),
136
+ # u4: Read 4 bytes as an unsigned number.
137
+ u4=lambda dvi, delta: dvi._read_arg(4, signed=False),
138
+ # s4: Read 4 bytes as a signed number.
139
+ s4=lambda dvi, delta: dvi._read_arg(4, signed=True),
140
+ # slen: Read delta bytes as a signed number, or None if delta is None.
141
+ slen=lambda dvi, delta: dvi._read_arg(delta, signed=True) if delta else None,
142
+ # slen1: Read (delta + 1) bytes as a signed number.
143
+ slen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=True),
144
+ # ulen1: Read (delta + 1) bytes as an unsigned number.
145
+ ulen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=False),
146
+ # olen1: Read (delta + 1) bytes as an unsigned number if less than 4 bytes,
147
+ # as a signed number if 4 bytes.
148
+ olen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=(delta == 3)),
149
+ )
150
+
151
+
152
+ def _dispatch(table, min, max=None, state=None, args=('raw',)):
153
+ """
154
+ Decorator for dispatch by opcode. Sets the values in *table*
155
+ from *min* to *max* to this method, adds a check that the Dvi state
156
+ matches *state* if not None, reads arguments from the file according
157
+ to *args*.
158
+
159
+ Parameters
160
+ ----------
161
+ table : dict[int, callable]
162
+ The dispatch table to be filled in.
163
+
164
+ min, max : int
165
+ Range of opcodes that calls the registered function; *max* defaults to
166
+ *min*.
167
+
168
+ state : _dvistate, optional
169
+ State of the Dvi object in which these opcodes are allowed.
170
+
171
+ args : list[str], default: ['raw']
172
+ Sequence of argument specifications:
173
+
174
+ - 'raw': opcode minus minimum
175
+ - 'u1': read one unsigned byte
176
+ - 'u4': read four bytes, treat as an unsigned number
177
+ - 's4': read four bytes, treat as a signed number
178
+ - 'slen': read (opcode - minimum) bytes, treat as signed
179
+ - 'slen1': read (opcode - minimum + 1) bytes, treat as signed
180
+ - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned
181
+ - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned
182
+ if under four bytes, signed if four bytes
183
+ """
184
+ def decorate(method):
185
+ get_args = [_arg_mapping[x] for x in args]
186
+
187
+ @wraps(method)
188
+ def wrapper(self, byte):
189
+ if state is not None and self.state != state:
190
+ raise ValueError("state precondition failed")
191
+ return method(self, *[f(self, byte-min) for f in get_args])
192
+ if max is None:
193
+ table[min] = wrapper
194
+ else:
195
+ for i in range(min, max+1):
196
+ assert table[i] is None
197
+ table[i] = wrapper
198
+ return wrapper
199
+ return decorate
200
+
201
+
202
+ class Dvi:
203
+ """
204
+ A reader for a dvi ("device-independent") file, as produced by TeX.
205
+
206
+ The current implementation can only iterate through pages in order,
207
+ and does not even attempt to verify the postamble.
208
+
209
+ This class can be used as a context manager to close the underlying
210
+ file upon exit. Pages can be read via iteration. Here is an overly
211
+ simple way to extract text without trying to detect whitespace::
212
+
213
+ >>> with matplotlib.dviread.Dvi('input.dvi', 72) as dvi:
214
+ ... for page in dvi:
215
+ ... print(''.join(chr(t.glyph) for t in page.text))
216
+ """
217
+ # dispatch table
218
+ _dtable = [None] * 256
219
+ _dispatch = partial(_dispatch, _dtable)
220
+
221
+ def __init__(self, filename, dpi):
222
+ """
223
+ Read the data from the file named *filename* and convert
224
+ TeX's internal units to units of *dpi* per inch.
225
+ *dpi* only sets the units and does not limit the resolution.
226
+ Use None to return TeX's internal units.
227
+ """
228
+ _log.debug('Dvi: %s', filename)
229
+ self.file = open(filename, 'rb')
230
+ self.dpi = dpi
231
+ self.fonts = {}
232
+ self.state = _dvistate.pre
233
+ self._missing_font = None
234
+
235
+ def __enter__(self):
236
+ """Context manager enter method, does nothing."""
237
+ return self
238
+
239
+ def __exit__(self, etype, evalue, etrace):
240
+ """
241
+ Context manager exit method, closes the underlying file if it is open.
242
+ """
243
+ self.close()
244
+
245
+ def __iter__(self):
246
+ """
247
+ Iterate through the pages of the file.
248
+
249
+ Yields
250
+ ------
251
+ Page
252
+ Details of all the text and box objects on the page.
253
+ The Page tuple contains lists of Text and Box tuples and
254
+ the page dimensions, and the Text and Box tuples contain
255
+ coordinates transformed into a standard Cartesian
256
+ coordinate system at the dpi value given when initializing.
257
+ The coordinates are floating point numbers, but otherwise
258
+ precision is not lost and coordinate values are not clipped to
259
+ integers.
260
+ """
261
+ while self._read():
262
+ yield self._output()
263
+
264
+ def close(self):
265
+ """Close the underlying file if it is open."""
266
+ if not self.file.closed:
267
+ self.file.close()
268
+
269
+ def _output(self):
270
+ """
271
+ Output the text and boxes belonging to the most recent page.
272
+ page = dvi._output()
273
+ """
274
+ minx = miny = np.inf
275
+ maxx = maxy = -np.inf
276
+ maxy_pure = -np.inf
277
+ for elt in self.text + self.boxes:
278
+ if isinstance(elt, Box):
279
+ x, y, h, w = elt
280
+ e = 0 # zero depth
281
+ else: # glyph
282
+ x, y, font, g, w = elt
283
+ h, e = font._height_depth_of(g)
284
+ minx = min(minx, x)
285
+ miny = min(miny, y - h)
286
+ maxx = max(maxx, x + w)
287
+ maxy = max(maxy, y + e)
288
+ maxy_pure = max(maxy_pure, y)
289
+ if self._baseline_v is not None:
290
+ maxy_pure = self._baseline_v # This should normally be the case.
291
+ self._baseline_v = None
292
+
293
+ if not self.text and not self.boxes: # Avoid infs/nans from inf+/-inf.
294
+ return Page(text=[], boxes=[], width=0, height=0, descent=0)
295
+
296
+ if self.dpi is None:
297
+ # special case for ease of debugging: output raw dvi coordinates
298
+ return Page(text=self.text, boxes=self.boxes,
299
+ width=maxx-minx, height=maxy_pure-miny,
300
+ descent=maxy-maxy_pure)
301
+
302
+ # convert from TeX's "scaled points" to dpi units
303
+ d = self.dpi / (72.27 * 2**16)
304
+ descent = (maxy - maxy_pure) * d
305
+
306
+ text = [Text((x-minx)*d, (maxy-y)*d - descent, f, g, w*d)
307
+ for (x, y, f, g, w) in self.text]
308
+ boxes = [Box((x-minx)*d, (maxy-y)*d - descent, h*d, w*d)
309
+ for (x, y, h, w) in self.boxes]
310
+
311
+ return Page(text=text, boxes=boxes, width=(maxx-minx)*d,
312
+ height=(maxy_pure-miny)*d, descent=descent)
313
+
314
+ def _read(self):
315
+ """
316
+ Read one page from the file. Return True if successful,
317
+ False if there were no more pages.
318
+ """
319
+ # Pages appear to start with the sequence
320
+ # bop (begin of page)
321
+ # xxx comment
322
+ # <push, ..., pop> # if using chemformula
323
+ # down
324
+ # push
325
+ # down
326
+ # <push, push, xxx, right, xxx, pop, pop> # if using xcolor
327
+ # down
328
+ # push
329
+ # down (possibly multiple)
330
+ # push <= here, v is the baseline position.
331
+ # etc.
332
+ # (dviasm is useful to explore this structure.)
333
+ # Thus, we use the vertical position at the first time the stack depth
334
+ # reaches 3, while at least three "downs" have been executed (excluding
335
+ # those popped out (corresponding to the chemformula preamble)), as the
336
+ # baseline (the "down" count is necessary to handle xcolor).
337
+ down_stack = [0]
338
+ self._baseline_v = None
339
+ while True:
340
+ byte = self.file.read(1)[0]
341
+ self._dtable[byte](self, byte)
342
+ if self._missing_font:
343
+ raise self._missing_font.to_exception()
344
+ name = self._dtable[byte].__name__
345
+ if name == "_push":
346
+ down_stack.append(down_stack[-1])
347
+ elif name == "_pop":
348
+ down_stack.pop()
349
+ elif name == "_down":
350
+ down_stack[-1] += 1
351
+ if (self._baseline_v is None
352
+ and len(getattr(self, "stack", [])) == 3
353
+ and down_stack[-1] >= 4):
354
+ self._baseline_v = self.v
355
+ if byte == 140: # end of page
356
+ return True
357
+ if self.state is _dvistate.post_post: # end of file
358
+ self.close()
359
+ return False
360
+
361
+ def _read_arg(self, nbytes, signed=False):
362
+ """
363
+ Read and return a big-endian integer *nbytes* long.
364
+ Signedness is determined by the *signed* keyword.
365
+ """
366
+ return int.from_bytes(self.file.read(nbytes), "big", signed=signed)
367
+
368
+ @_dispatch(min=0, max=127, state=_dvistate.inpage)
369
+ def _set_char_immediate(self, char):
370
+ self._put_char_real(char)
371
+ if isinstance(self.fonts[self.f], cbook._ExceptionInfo):
372
+ return
373
+ self.h += self.fonts[self.f]._width_of(char)
374
+
375
+ @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',))
376
+ def _set_char(self, char):
377
+ self._put_char_real(char)
378
+ if isinstance(self.fonts[self.f], cbook._ExceptionInfo):
379
+ return
380
+ self.h += self.fonts[self.f]._width_of(char)
381
+
382
+ @_dispatch(132, state=_dvistate.inpage, args=('s4', 's4'))
383
+ def _set_rule(self, a, b):
384
+ self._put_rule_real(a, b)
385
+ self.h += b
386
+
387
+ @_dispatch(min=133, max=136, state=_dvistate.inpage, args=('olen1',))
388
+ def _put_char(self, char):
389
+ self._put_char_real(char)
390
+
391
+ def _put_char_real(self, char):
392
+ font = self.fonts[self.f]
393
+ if isinstance(font, cbook._ExceptionInfo):
394
+ self._missing_font = font
395
+ elif font._vf is None:
396
+ self.text.append(Text(self.h, self.v, font, char,
397
+ font._width_of(char)))
398
+ else:
399
+ scale = font._scale
400
+ for x, y, f, g, w in font._vf[char].text:
401
+ newf = DviFont(scale=_mul2012(scale, f._scale),
402
+ tfm=f._tfm, texname=f.texname, vf=f._vf)
403
+ self.text.append(Text(self.h + _mul2012(x, scale),
404
+ self.v + _mul2012(y, scale),
405
+ newf, g, newf._width_of(g)))
406
+ self.boxes.extend([Box(self.h + _mul2012(x, scale),
407
+ self.v + _mul2012(y, scale),
408
+ _mul2012(a, scale), _mul2012(b, scale))
409
+ for x, y, a, b in font._vf[char].boxes])
410
+
411
+ @_dispatch(137, state=_dvistate.inpage, args=('s4', 's4'))
412
+ def _put_rule(self, a, b):
413
+ self._put_rule_real(a, b)
414
+
415
+ def _put_rule_real(self, a, b):
416
+ if a > 0 and b > 0:
417
+ self.boxes.append(Box(self.h, self.v, a, b))
418
+
419
+ @_dispatch(138)
420
+ def _nop(self, _):
421
+ pass
422
+
423
+ @_dispatch(139, state=_dvistate.outer, args=('s4',)*11)
424
+ def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p):
425
+ self.state = _dvistate.inpage
426
+ self.h = self.v = self.w = self.x = self.y = self.z = 0
427
+ self.stack = []
428
+ self.text = [] # list of Text objects
429
+ self.boxes = [] # list of Box objects
430
+
431
+ @_dispatch(140, state=_dvistate.inpage)
432
+ def _eop(self, _):
433
+ self.state = _dvistate.outer
434
+ del self.h, self.v, self.w, self.x, self.y, self.z, self.stack
435
+
436
+ @_dispatch(141, state=_dvistate.inpage)
437
+ def _push(self, _):
438
+ self.stack.append((self.h, self.v, self.w, self.x, self.y, self.z))
439
+
440
+ @_dispatch(142, state=_dvistate.inpage)
441
+ def _pop(self, _):
442
+ self.h, self.v, self.w, self.x, self.y, self.z = self.stack.pop()
443
+
444
+ @_dispatch(min=143, max=146, state=_dvistate.inpage, args=('slen1',))
445
+ def _right(self, b):
446
+ self.h += b
447
+
448
+ @_dispatch(min=147, max=151, state=_dvistate.inpage, args=('slen',))
449
+ def _right_w(self, new_w):
450
+ if new_w is not None:
451
+ self.w = new_w
452
+ self.h += self.w
453
+
454
+ @_dispatch(min=152, max=156, state=_dvistate.inpage, args=('slen',))
455
+ def _right_x(self, new_x):
456
+ if new_x is not None:
457
+ self.x = new_x
458
+ self.h += self.x
459
+
460
+ @_dispatch(min=157, max=160, state=_dvistate.inpage, args=('slen1',))
461
+ def _down(self, a):
462
+ self.v += a
463
+
464
+ @_dispatch(min=161, max=165, state=_dvistate.inpage, args=('slen',))
465
+ def _down_y(self, new_y):
466
+ if new_y is not None:
467
+ self.y = new_y
468
+ self.v += self.y
469
+
470
+ @_dispatch(min=166, max=170, state=_dvistate.inpage, args=('slen',))
471
+ def _down_z(self, new_z):
472
+ if new_z is not None:
473
+ self.z = new_z
474
+ self.v += self.z
475
+
476
+ @_dispatch(min=171, max=234, state=_dvistate.inpage)
477
+ def _fnt_num_immediate(self, k):
478
+ self.f = k
479
+
480
+ @_dispatch(min=235, max=238, state=_dvistate.inpage, args=('olen1',))
481
+ def _fnt_num(self, new_f):
482
+ self.f = new_f
483
+
484
+ @_dispatch(min=239, max=242, args=('ulen1',))
485
+ def _xxx(self, datalen):
486
+ special = self.file.read(datalen)
487
+ _log.debug(
488
+ 'Dvi._xxx: encountered special: %s',
489
+ ''.join([chr(ch) if 32 <= ch < 127 else '<%02x>' % ch
490
+ for ch in special]))
491
+
492
+ @_dispatch(min=243, max=246, args=('olen1', 'u4', 'u4', 'u4', 'u1', 'u1'))
493
+ def _fnt_def(self, k, c, s, d, a, l):
494
+ self._fnt_def_real(k, c, s, d, a, l)
495
+
496
+ def _fnt_def_real(self, k, c, s, d, a, l):
497
+ n = self.file.read(a + l)
498
+ fontname = n[-l:].decode('ascii')
499
+ try:
500
+ tfm = _tfmfile(fontname)
501
+ except FileNotFoundError as exc:
502
+ # Explicitly allow defining missing fonts for Vf support; we only
503
+ # register an error when trying to load a glyph from a missing font
504
+ # and throw that error in Dvi._read. For Vf, _finalize_packet
505
+ # checks whether a missing glyph has been used, and in that case
506
+ # skips the glyph definition.
507
+ self.fonts[k] = cbook._ExceptionInfo.from_exception(exc)
508
+ return
509
+ if c != 0 and tfm.checksum != 0 and c != tfm.checksum:
510
+ raise ValueError(f'tfm checksum mismatch: {n}')
511
+ try:
512
+ vf = _vffile(fontname)
513
+ except FileNotFoundError:
514
+ vf = None
515
+ self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf)
516
+
517
+ @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1'))
518
+ def _pre(self, i, num, den, mag, k):
519
+ self.file.read(k) # comment in the dvi file
520
+ if i != 2:
521
+ raise ValueError(f"Unknown dvi format {i}")
522
+ if num != 25400000 or den != 7227 * 2**16:
523
+ raise ValueError("Nonstandard units in dvi file")
524
+ # meaning: TeX always uses those exact values, so it
525
+ # should be enough for us to support those
526
+ # (There are 72.27 pt to an inch so 7227 pt =
527
+ # 7227 * 2**16 sp to 100 in. The numerator is multiplied
528
+ # by 10^5 to get units of 10**-7 meters.)
529
+ if mag != 1000:
530
+ raise ValueError("Nonstandard magnification in dvi file")
531
+ # meaning: LaTeX seems to frown on setting \mag, so
532
+ # I think we can assume this is constant
533
+ self.state = _dvistate.outer
534
+
535
+ @_dispatch(248, state=_dvistate.outer)
536
+ def _post(self, _):
537
+ self.state = _dvistate.post_post
538
+ # TODO: actually read the postamble and finale?
539
+ # currently post_post just triggers closing the file
540
+
541
+ @_dispatch(249)
542
+ def _post_post(self, _):
543
+ raise NotImplementedError
544
+
545
+ @_dispatch(min=250, max=255)
546
+ def _malformed(self, offset):
547
+ raise ValueError(f"unknown command: byte {250 + offset}")
548
+
549
+
550
+ class DviFont:
551
+ """
552
+ Encapsulation of a font that a DVI file can refer to.
553
+
554
+ This class holds a font's texname and size, supports comparison,
555
+ and knows the widths of glyphs in the same units as the AFM file.
556
+ There are also internal attributes (for use by dviread.py) that
557
+ are *not* used for comparison.
558
+
559
+ The size is in Adobe points (converted from TeX points).
560
+
561
+ Parameters
562
+ ----------
563
+ scale : float
564
+ Factor by which the font is scaled from its natural size.
565
+ tfm : Tfm
566
+ TeX font metrics for this font
567
+ texname : bytes
568
+ Name of the font as used internally by TeX and friends, as an ASCII
569
+ bytestring. This is usually very different from any external font
570
+ names; `PsfontsMap` can be used to find the external name of the font.
571
+ vf : Vf
572
+ A TeX "virtual font" file, or None if this font is not virtual.
573
+
574
+ Attributes
575
+ ----------
576
+ texname : bytes
577
+ size : float
578
+ Size of the font in Adobe points, converted from the slightly
579
+ smaller TeX points.
580
+ widths : list
581
+ Widths of glyphs in glyph-space units, typically 1/1000ths of
582
+ the point size.
583
+
584
+ """
585
+ __slots__ = ('texname', 'size', 'widths', '_scale', '_vf', '_tfm')
586
+
587
+ def __init__(self, scale, tfm, texname, vf):
588
+ _api.check_isinstance(bytes, texname=texname)
589
+ self._scale = scale
590
+ self._tfm = tfm
591
+ self.texname = texname
592
+ self._vf = vf
593
+ self.size = scale * (72.0 / (72.27 * 2**16))
594
+ try:
595
+ nchars = max(tfm.width) + 1
596
+ except ValueError:
597
+ nchars = 0
598
+ self.widths = [(1000*tfm.width.get(char, 0)) >> 20
599
+ for char in range(nchars)]
600
+
601
+ def __eq__(self, other):
602
+ return (type(self) is type(other)
603
+ and self.texname == other.texname and self.size == other.size)
604
+
605
+ def __ne__(self, other):
606
+ return not self.__eq__(other)
607
+
608
+ def __repr__(self):
609
+ return f"<{type(self).__name__}: {self.texname}>"
610
+
611
+ def _width_of(self, char):
612
+ """Width of char in dvi units."""
613
+ width = self._tfm.width.get(char, None)
614
+ if width is not None:
615
+ return _mul2012(width, self._scale)
616
+ _log.debug('No width for char %d in font %s.', char, self.texname)
617
+ return 0
618
+
619
+ def _height_depth_of(self, char):
620
+ """Height and depth of char in dvi units."""
621
+ result = []
622
+ for metric, name in ((self._tfm.height, "height"),
623
+ (self._tfm.depth, "depth")):
624
+ value = metric.get(char, None)
625
+ if value is None:
626
+ _log.debug('No %s for char %d in font %s',
627
+ name, char, self.texname)
628
+ result.append(0)
629
+ else:
630
+ result.append(_mul2012(value, self._scale))
631
+ # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent
632
+ # so that TeX aligns equations properly
633
+ # (https://tex.stackexchange.com/q/526103/)
634
+ # but we actually care about the rasterization depth to align
635
+ # the dvipng-generated images.
636
+ if re.match(br'^cmsy\d+$', self.texname) and char == 0:
637
+ result[-1] = 0
638
+ return result
639
+
640
+
641
+ class Vf(Dvi):
642
+ r"""
643
+ A virtual font (\*.vf file) containing subroutines for dvi files.
644
+
645
+ Parameters
646
+ ----------
647
+ filename : str or path-like
648
+
649
+ Notes
650
+ -----
651
+ The virtual font format is a derivative of dvi:
652
+ http://mirrors.ctan.org/info/knuth/virtual-fonts
653
+ This class reuses some of the machinery of `Dvi`
654
+ but replaces the `_read` loop and dispatch mechanism.
655
+
656
+ Examples
657
+ --------
658
+ ::
659
+
660
+ vf = Vf(filename)
661
+ glyph = vf[code]
662
+ glyph.text, glyph.boxes, glyph.width
663
+ """
664
+
665
+ def __init__(self, filename):
666
+ super().__init__(filename, 0)
667
+ try:
668
+ self._first_font = None
669
+ self._chars = {}
670
+ self._read()
671
+ finally:
672
+ self.close()
673
+
674
+ def __getitem__(self, code):
675
+ return self._chars[code]
676
+
677
+ def _read(self):
678
+ """
679
+ Read one page from the file. Return True if successful,
680
+ False if there were no more pages.
681
+ """
682
+ packet_char = packet_ends = None
683
+ packet_len = packet_width = None
684
+ while True:
685
+ byte = self.file.read(1)[0]
686
+ # If we are in a packet, execute the dvi instructions
687
+ if self.state is _dvistate.inpage:
688
+ byte_at = self.file.tell()-1
689
+ if byte_at == packet_ends:
690
+ self._finalize_packet(packet_char, packet_width)
691
+ packet_len = packet_char = packet_width = None
692
+ # fall through to out-of-packet code
693
+ elif byte_at > packet_ends:
694
+ raise ValueError("Packet length mismatch in vf file")
695
+ else:
696
+ if byte in (139, 140) or byte >= 243:
697
+ raise ValueError(f"Inappropriate opcode {byte} in vf file")
698
+ Dvi._dtable[byte](self, byte)
699
+ continue
700
+
701
+ # We are outside a packet
702
+ if byte < 242: # a short packet (length given by byte)
703
+ packet_len = byte
704
+ packet_char = self._read_arg(1)
705
+ packet_width = self._read_arg(3)
706
+ packet_ends = self._init_packet(byte)
707
+ self.state = _dvistate.inpage
708
+ elif byte == 242: # a long packet
709
+ packet_len = self._read_arg(4)
710
+ packet_char = self._read_arg(4)
711
+ packet_width = self._read_arg(4)
712
+ self._init_packet(packet_len)
713
+ elif 243 <= byte <= 246:
714
+ k = self._read_arg(byte - 242, byte == 246)
715
+ c = self._read_arg(4)
716
+ s = self._read_arg(4)
717
+ d = self._read_arg(4)
718
+ a = self._read_arg(1)
719
+ l = self._read_arg(1)
720
+ self._fnt_def_real(k, c, s, d, a, l)
721
+ if self._first_font is None:
722
+ self._first_font = k
723
+ elif byte == 247: # preamble
724
+ i = self._read_arg(1)
725
+ k = self._read_arg(1)
726
+ x = self.file.read(k)
727
+ cs = self._read_arg(4)
728
+ ds = self._read_arg(4)
729
+ self._pre(i, x, cs, ds)
730
+ elif byte == 248: # postamble (just some number of 248s)
731
+ break
732
+ else:
733
+ raise ValueError(f"Unknown vf opcode {byte}")
734
+
735
+ def _init_packet(self, pl):
736
+ if self.state != _dvistate.outer:
737
+ raise ValueError("Misplaced packet in vf file")
738
+ self.h = self.v = self.w = self.x = self.y = self.z = 0
739
+ self.stack = []
740
+ self.text = []
741
+ self.boxes = []
742
+ self.f = self._first_font
743
+ self._missing_font = None
744
+ return self.file.tell() + pl
745
+
746
+ def _finalize_packet(self, packet_char, packet_width):
747
+ if not self._missing_font: # Otherwise we don't have full glyph definition.
748
+ self._chars[packet_char] = Page(
749
+ text=self.text, boxes=self.boxes, width=packet_width,
750
+ height=None, descent=None)
751
+ self.state = _dvistate.outer
752
+
753
+ def _pre(self, i, x, cs, ds):
754
+ if self.state is not _dvistate.pre:
755
+ raise ValueError("pre command in middle of vf file")
756
+ if i != 202:
757
+ raise ValueError(f"Unknown vf format {i}")
758
+ if len(x):
759
+ _log.debug('vf file comment: %s', x)
760
+ self.state = _dvistate.outer
761
+ # cs = checksum, ds = design size
762
+
763
+
764
+ def _mul2012(num1, num2):
765
+ """Multiply two numbers in 20.12 fixed point format."""
766
+ # Separated into a function because >> has surprising precedence
767
+ return (num1*num2) >> 20
768
+
769
+
770
+ class Tfm:
771
+ """
772
+ A TeX Font Metric file.
773
+
774
+ This implementation covers only the bare minimum needed by the Dvi class.
775
+
776
+ Parameters
777
+ ----------
778
+ filename : str or path-like
779
+
780
+ Attributes
781
+ ----------
782
+ checksum : int
783
+ Used for verifying against the dvi file.
784
+ design_size : int
785
+ Design size of the font (unknown units)
786
+ width, height, depth : dict
787
+ Dimensions of each character, need to be scaled by the factor
788
+ specified in the dvi file. These are dicts because indexing may
789
+ not start from 0.
790
+ """
791
+ __slots__ = ('checksum', 'design_size', 'width', 'height', 'depth')
792
+
793
+ def __init__(self, filename):
794
+ _log.debug('opening tfm file %s', filename)
795
+ with open(filename, 'rb') as file:
796
+ header1 = file.read(24)
797
+ lh, bc, ec, nw, nh, nd = struct.unpack('!6H', header1[2:14])
798
+ _log.debug('lh=%d, bc=%d, ec=%d, nw=%d, nh=%d, nd=%d',
799
+ lh, bc, ec, nw, nh, nd)
800
+ header2 = file.read(4*lh)
801
+ self.checksum, self.design_size = struct.unpack('!2I', header2[:8])
802
+ # there is also encoding information etc.
803
+ char_info = file.read(4*(ec-bc+1))
804
+ widths = struct.unpack(f'!{nw}i', file.read(4*nw))
805
+ heights = struct.unpack(f'!{nh}i', file.read(4*nh))
806
+ depths = struct.unpack(f'!{nd}i', file.read(4*nd))
807
+ self.width = {}
808
+ self.height = {}
809
+ self.depth = {}
810
+ for idx, char in enumerate(range(bc, ec+1)):
811
+ byte0 = char_info[4*idx]
812
+ byte1 = char_info[4*idx+1]
813
+ self.width[char] = widths[byte0]
814
+ self.height[char] = heights[byte1 >> 4]
815
+ self.depth[char] = depths[byte1 & 0xf]
816
+
817
+
818
+ PsFont = namedtuple('PsFont', 'texname psname effects encoding filename')
819
+
820
+
821
+ class PsfontsMap:
822
+ """
823
+ A psfonts.map formatted file, mapping TeX fonts to PS fonts.
824
+
825
+ Parameters
826
+ ----------
827
+ filename : str or path-like
828
+
829
+ Notes
830
+ -----
831
+ For historical reasons, TeX knows many Type-1 fonts by different
832
+ names than the outside world. (For one thing, the names have to
833
+ fit in eight characters.) Also, TeX's native fonts are not Type-1
834
+ but Metafont, which is nontrivial to convert to PostScript except
835
+ as a bitmap. While high-quality conversions to Type-1 format exist
836
+ and are shipped with modern TeX distributions, we need to know
837
+ which Type-1 fonts are the counterparts of which native fonts. For
838
+ these reasons a mapping is needed from internal font names to font
839
+ file names.
840
+
841
+ A texmf tree typically includes mapping files called e.g.
842
+ :file:`psfonts.map`, :file:`pdftex.map`, or :file:`dvipdfm.map`.
843
+ The file :file:`psfonts.map` is used by :program:`dvips`,
844
+ :file:`pdftex.map` by :program:`pdfTeX`, and :file:`dvipdfm.map`
845
+ by :program:`dvipdfm`. :file:`psfonts.map` might avoid embedding
846
+ the 35 PostScript fonts (i.e., have no filename for them, as in
847
+ the Times-Bold example above), while the pdf-related files perhaps
848
+ only avoid the "Base 14" pdf fonts. But the user may have
849
+ configured these files differently.
850
+
851
+ Examples
852
+ --------
853
+ >>> map = PsfontsMap(find_tex_file('pdftex.map'))
854
+ >>> entry = map[b'ptmbo8r']
855
+ >>> entry.texname
856
+ b'ptmbo8r'
857
+ >>> entry.psname
858
+ b'Times-Bold'
859
+ >>> entry.encoding
860
+ '/usr/local/texlive/2008/texmf-dist/fonts/enc/dvips/base/8r.enc'
861
+ >>> entry.effects
862
+ {'slant': 0.16700000000000001}
863
+ >>> entry.filename
864
+ """
865
+ __slots__ = ('_filename', '_unparsed', '_parsed')
866
+
867
+ # Create a filename -> PsfontsMap cache, so that calling
868
+ # `PsfontsMap(filename)` with the same filename a second time immediately
869
+ # returns the same object.
870
+ @lru_cache
871
+ def __new__(cls, filename):
872
+ self = object.__new__(cls)
873
+ self._filename = os.fsdecode(filename)
874
+ # Some TeX distributions have enormous pdftex.map files which would
875
+ # take hundreds of milliseconds to parse, but it is easy enough to just
876
+ # store the unparsed lines (keyed by the first word, which is the
877
+ # texname) and parse them on-demand.
878
+ with open(filename, 'rb') as file:
879
+ self._unparsed = {}
880
+ for line in file:
881
+ tfmname = line.split(b' ', 1)[0]
882
+ self._unparsed.setdefault(tfmname, []).append(line)
883
+ self._parsed = {}
884
+ return self
885
+
886
+ def __getitem__(self, texname):
887
+ assert isinstance(texname, bytes)
888
+ if texname in self._unparsed:
889
+ for line in self._unparsed.pop(texname):
890
+ if self._parse_and_cache_line(line):
891
+ break
892
+ try:
893
+ return self._parsed[texname]
894
+ except KeyError:
895
+ raise LookupError(
896
+ f"An associated PostScript font (required by Matplotlib) "
897
+ f"could not be found for TeX font {texname.decode('ascii')!r} "
898
+ f"in {self._filename!r}; this problem can often be solved by "
899
+ f"installing a suitable PostScript font package in your TeX "
900
+ f"package manager") from None
901
+
902
+ def _parse_and_cache_line(self, line):
903
+ """
904
+ Parse a line in the font mapping file.
905
+
906
+ The format is (partially) documented at
907
+ http://mirrors.ctan.org/systems/doc/pdftex/manual/pdftex-a.pdf
908
+ https://tug.org/texinfohtml/dvips.html#psfonts_002emap
909
+ Each line can have the following fields:
910
+
911
+ - tfmname (first, only required field),
912
+ - psname (defaults to tfmname, must come immediately after tfmname if
913
+ present),
914
+ - fontflags (integer, must come immediately after psname if present,
915
+ ignored by us),
916
+ - special (SlantFont and ExtendFont, only field that is double-quoted),
917
+ - fontfile, encodingfile (optional, prefixed by <, <<, or <[; << always
918
+ precedes a font, <[ always precedes an encoding, < can precede either
919
+ but then an encoding file must have extension .enc; < and << also
920
+ request different font subsetting behaviors but we ignore that; < can
921
+ be separated from the filename by whitespace).
922
+
923
+ special, fontfile, and encodingfile can appear in any order.
924
+ """
925
+ # If the map file specifies multiple encodings for a font, we
926
+ # follow pdfTeX in choosing the last one specified. Such
927
+ # entries are probably mistakes but they have occurred.
928
+ # https://tex.stackexchange.com/q/10826/
929
+
930
+ if not line or line.startswith((b" ", b"%", b"*", b";", b"#")):
931
+ return
932
+ tfmname = basename = special = encodingfile = fontfile = None
933
+ is_subsetted = is_t1 = is_truetype = False
934
+ matches = re.finditer(br'"([^"]*)(?:"|$)|(\S+)', line)
935
+ for match in matches:
936
+ quoted, unquoted = match.groups()
937
+ if unquoted:
938
+ if unquoted.startswith(b"<<"): # font
939
+ fontfile = unquoted[2:]
940
+ elif unquoted.startswith(b"<["): # encoding
941
+ encodingfile = unquoted[2:]
942
+ elif unquoted.startswith(b"<"): # font or encoding
943
+ word = (
944
+ # <foo => foo
945
+ unquoted[1:]
946
+ # < by itself => read the next word
947
+ or next(filter(None, next(matches).groups())))
948
+ if word.endswith(b".enc"):
949
+ encodingfile = word
950
+ else:
951
+ fontfile = word
952
+ is_subsetted = True
953
+ elif tfmname is None:
954
+ tfmname = unquoted
955
+ elif basename is None:
956
+ basename = unquoted
957
+ elif quoted:
958
+ special = quoted
959
+ effects = {}
960
+ if special:
961
+ words = reversed(special.split())
962
+ for word in words:
963
+ if word == b"SlantFont":
964
+ effects["slant"] = float(next(words))
965
+ elif word == b"ExtendFont":
966
+ effects["extend"] = float(next(words))
967
+
968
+ # Verify some properties of the line that would cause it to be ignored
969
+ # otherwise.
970
+ if fontfile is not None:
971
+ if fontfile.endswith((b".ttf", b".ttc")):
972
+ is_truetype = True
973
+ elif not fontfile.endswith(b".otf"):
974
+ is_t1 = True
975
+ elif basename is not None:
976
+ is_t1 = True
977
+ if is_truetype and is_subsetted and encodingfile is None:
978
+ return
979
+ if not is_t1 and ("slant" in effects or "extend" in effects):
980
+ return
981
+ if abs(effects.get("slant", 0)) > 1:
982
+ return
983
+ if abs(effects.get("extend", 0)) > 2:
984
+ return
985
+
986
+ if basename is None:
987
+ basename = tfmname
988
+ if encodingfile is not None:
989
+ encodingfile = find_tex_file(encodingfile)
990
+ if fontfile is not None:
991
+ fontfile = find_tex_file(fontfile)
992
+ self._parsed[tfmname] = PsFont(
993
+ texname=tfmname, psname=basename, effects=effects,
994
+ encoding=encodingfile, filename=fontfile)
995
+ return True
996
+
997
+
998
+ def _parse_enc(path):
999
+ r"""
1000
+ Parse a \*.enc file referenced from a psfonts.map style file.
1001
+
1002
+ The format supported by this function is a tiny subset of PostScript.
1003
+
1004
+ Parameters
1005
+ ----------
1006
+ path : `os.PathLike`
1007
+
1008
+ Returns
1009
+ -------
1010
+ list
1011
+ The nth entry of the list is the PostScript glyph name of the nth
1012
+ glyph.
1013
+ """
1014
+ no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii"))
1015
+ array = re.search(r"(?s)\[(.*)\]", no_comments).group(1)
1016
+ lines = [line for line in array.split() if line]
1017
+ if all(line.startswith("/") for line in lines):
1018
+ return [line[1:] for line in lines]
1019
+ else:
1020
+ raise ValueError(f"Failed to parse {path} as Postscript encoding")
1021
+
1022
+
1023
+ class _LuatexKpsewhich:
1024
+ @lru_cache # A singleton.
1025
+ def __new__(cls):
1026
+ self = object.__new__(cls)
1027
+ self._proc = self._new_proc()
1028
+ return self
1029
+
1030
+ def _new_proc(self):
1031
+ return subprocess.Popen(
1032
+ ["luatex", "--luaonly",
1033
+ str(cbook._get_data_path("kpsewhich.lua"))],
1034
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1035
+
1036
+ def search(self, filename):
1037
+ if self._proc.poll() is not None: # Dead, restart it.
1038
+ self._proc = self._new_proc()
1039
+ self._proc.stdin.write(os.fsencode(filename) + b"\n")
1040
+ self._proc.stdin.flush()
1041
+ out = self._proc.stdout.readline().rstrip()
1042
+ return None if out == b"nil" else os.fsdecode(out)
1043
+
1044
+
1045
+ @lru_cache
1046
+ def find_tex_file(filename):
1047
+ """
1048
+ Find a file in the texmf tree using kpathsea_.
1049
+
1050
+ The kpathsea library, provided by most existing TeX distributions, both
1051
+ on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived
1052
+ luatex process if luatex is installed, or via kpsewhich otherwise.
1053
+
1054
+ .. _kpathsea: https://www.tug.org/kpathsea/
1055
+
1056
+ Parameters
1057
+ ----------
1058
+ filename : str or path-like
1059
+
1060
+ Raises
1061
+ ------
1062
+ FileNotFoundError
1063
+ If the file is not found.
1064
+ """
1065
+
1066
+ # we expect these to always be ascii encoded, but use utf-8
1067
+ # out of caution
1068
+ if isinstance(filename, bytes):
1069
+ filename = filename.decode('utf-8', errors='replace')
1070
+
1071
+ try:
1072
+ lk = _LuatexKpsewhich()
1073
+ except FileNotFoundError:
1074
+ lk = None # Fallback to directly calling kpsewhich, as below.
1075
+
1076
+ if lk:
1077
+ path = lk.search(filename)
1078
+ else:
1079
+ if sys.platform == 'win32':
1080
+ # On Windows only, kpathsea can use utf-8 for cmd args and output.
1081
+ # The `command_line_encoding` environment variable is set to force
1082
+ # it to always use utf-8 encoding. See Matplotlib issue #11848.
1083
+ kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'},
1084
+ 'encoding': 'utf-8'}
1085
+ else: # On POSIX, run through the equivalent of os.fsdecode().
1086
+ kwargs = {'encoding': sys.getfilesystemencoding(),
1087
+ 'errors': 'surrogateescape'}
1088
+
1089
+ try:
1090
+ path = (cbook._check_and_log_subprocess(['kpsewhich', filename],
1091
+ _log, **kwargs)
1092
+ .rstrip('\n'))
1093
+ except (FileNotFoundError, RuntimeError):
1094
+ path = None
1095
+
1096
+ if path:
1097
+ return path
1098
+ else:
1099
+ raise FileNotFoundError(
1100
+ f"Matplotlib's TeX implementation searched for a file named "
1101
+ f"{filename!r} in your texmf tree, but could not find it")
1102
+
1103
+
1104
+ @lru_cache
1105
+ def _fontfile(cls, suffix, texname):
1106
+ return cls(find_tex_file(texname + suffix))
1107
+
1108
+
1109
+ _tfmfile = partial(_fontfile, Tfm, ".tfm")
1110
+ _vffile = partial(_fontfile, Vf, ".vf")
1111
+
1112
+
1113
+ if __name__ == '__main__':
1114
+ from argparse import ArgumentParser
1115
+ import itertools
1116
+
1117
+ parser = ArgumentParser()
1118
+ parser.add_argument("filename")
1119
+ parser.add_argument("dpi", nargs="?", type=float, default=None)
1120
+ args = parser.parse_args()
1121
+ with Dvi(args.filename, args.dpi) as dvi:
1122
+ fontmap = PsfontsMap(find_tex_file('pdftex.map'))
1123
+ for page in dvi:
1124
+ print(f"=== new page === "
1125
+ f"(w: {page.width}, h: {page.height}, d: {page.descent})")
1126
+ for font, group in itertools.groupby(
1127
+ page.text, lambda text: text.font):
1128
+ print(f"font: {font.texname.decode('latin-1')!r}\t"
1129
+ f"scale: {font._scale / 2 ** 20}")
1130
+ print("x", "y", "glyph", "chr", "w", "(glyphs)", sep="\t")
1131
+ for text in group:
1132
+ print(text.x, text.y, text.glyph,
1133
+ chr(text.glyph) if chr(text.glyph).isprintable()
1134
+ else ".",
1135
+ text.width, sep="\t")
1136
+ if page.boxes:
1137
+ print("x", "y", "h", "w", "", "(boxes)", sep="\t")
1138
+ for box in page.boxes:
1139
+ print(box.x, box.y, box.height, box.width, sep="\t")
lib/python3.12/site-packages/matplotlib/dviread.pyi ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import io
3
+ import os
4
+ from enum import Enum
5
+ from collections.abc import Generator
6
+
7
+ from typing import NamedTuple
8
+ from typing_extensions import Self # < Py 3.11
9
+
10
+ class _dvistate(Enum):
11
+ pre = ...
12
+ outer = ...
13
+ inpage = ...
14
+ post_post = ...
15
+ finale = ...
16
+
17
+ class Page(NamedTuple):
18
+ text: list[Text]
19
+ boxes: list[Box]
20
+ height: int
21
+ width: int
22
+ descent: int
23
+
24
+ class Box(NamedTuple):
25
+ x: int
26
+ y: int
27
+ height: int
28
+ width: int
29
+
30
+ class Text(NamedTuple):
31
+ x: int
32
+ y: int
33
+ font: DviFont
34
+ glyph: int
35
+ width: int
36
+ @property
37
+ def font_path(self) -> Path: ...
38
+ @property
39
+ def font_size(self) -> float: ...
40
+ @property
41
+ def font_effects(self) -> dict[str, float]: ...
42
+ @property
43
+ def glyph_name_or_index(self) -> int | str: ...
44
+
45
+ class Dvi:
46
+ file: io.BufferedReader
47
+ dpi: float | None
48
+ fonts: dict[int, DviFont]
49
+ state: _dvistate
50
+ def __init__(self, filename: str | os.PathLike, dpi: float | None) -> None: ...
51
+ def __enter__(self) -> Self: ...
52
+ def __exit__(self, etype, evalue, etrace) -> None: ...
53
+ def __iter__(self) -> Generator[Page, None, None]: ...
54
+ def close(self) -> None: ...
55
+
56
+ class DviFont:
57
+ texname: bytes
58
+ size: float
59
+ widths: list[int]
60
+ def __init__(
61
+ self, scale: float, tfm: Tfm, texname: bytes, vf: Vf | None
62
+ ) -> None: ...
63
+ def __eq__(self, other: object) -> bool: ...
64
+ def __ne__(self, other: object) -> bool: ...
65
+
66
+ class Vf(Dvi):
67
+ def __init__(self, filename: str | os.PathLike) -> None: ...
68
+ def __getitem__(self, code: int) -> Page: ...
69
+
70
+ class Tfm:
71
+ checksum: int
72
+ design_size: int
73
+ width: dict[int, int]
74
+ height: dict[int, int]
75
+ depth: dict[int, int]
76
+ def __init__(self, filename: str | os.PathLike) -> None: ...
77
+
78
+ class PsFont(NamedTuple):
79
+ texname: bytes
80
+ psname: bytes
81
+ effects: dict[str, float]
82
+ encoding: None | bytes
83
+ filename: str
84
+
85
+ class PsfontsMap:
86
+ def __new__(cls, filename: str | os.PathLike) -> Self: ...
87
+ def __getitem__(self, texname: bytes) -> PsFont: ...
88
+
89
+ def find_tex_file(filename: str | os.PathLike) -> str: ...
lib/python3.12/site-packages/matplotlib/inset.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ The inset module defines the InsetIndicator class, which draws the rectangle and
3
+ connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`.
4
+ """
5
+
6
+ from . import _api, artist, transforms
7
+ from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle
8
+ from matplotlib.path import Path
9
+
10
+
11
+ _shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth')
12
+
13
+
14
+ class InsetIndicator(artist.Artist):
15
+ """
16
+ An artist to highlight an area of interest.
17
+
18
+ An inset indicator is a rectangle on the plot at the position indicated by
19
+ *bounds* that optionally has lines that connect the rectangle to an inset
20
+ Axes (`.Axes.inset_axes`).
21
+
22
+ .. versionadded:: 3.10
23
+ """
24
+ zorder = 4.99
25
+
26
+ def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs):
27
+ """
28
+ Parameters
29
+ ----------
30
+ bounds : [x0, y0, width, height], optional
31
+ Lower-left corner of rectangle to be marked, and its width
32
+ and height. If not set, the bounds will be calculated from the
33
+ data limits of inset_ax, which must be supplied.
34
+
35
+ inset_ax : `~.axes.Axes`, optional
36
+ An optional inset Axes to draw connecting lines to. Two lines are
37
+ drawn connecting the indicator box to the inset Axes on corners
38
+ chosen so as to not overlap with the indicator box.
39
+
40
+ zorder : float, default: 4.99
41
+ Drawing order of the rectangle and connector lines. The default,
42
+ 4.99, is just below the default level of inset Axes.
43
+
44
+ **kwargs
45
+ Other keyword arguments are passed on to the `.Rectangle` patch.
46
+ """
47
+ if bounds is None and inset_ax is None:
48
+ raise ValueError("At least one of bounds or inset_ax must be supplied")
49
+
50
+ self._inset_ax = inset_ax
51
+
52
+ if bounds is None:
53
+ # Work out bounds from inset_ax
54
+ self._auto_update_bounds = True
55
+ bounds = self._bounds_from_inset_ax()
56
+ else:
57
+ self._auto_update_bounds = False
58
+
59
+ x, y, width, height = bounds
60
+
61
+ self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs)
62
+
63
+ # Connector positions cannot be calculated till the artist has been added
64
+ # to an axes, so just make an empty list for now.
65
+ self._connectors = []
66
+
67
+ super().__init__()
68
+ self.set_zorder(zorder)
69
+
70
+ # Initial style properties for the artist should match the rectangle.
71
+ for prop in _shared_properties:
72
+ setattr(self, f'_{prop}', artist.getp(self._rectangle, prop))
73
+
74
+ def _shared_setter(self, prop, val):
75
+ """
76
+ Helper function to set the same style property on the artist and its children.
77
+ """
78
+ setattr(self, f'_{prop}', val)
79
+
80
+ artist.setp([self._rectangle, *self._connectors], prop, val)
81
+
82
+ def set_alpha(self, alpha):
83
+ # docstring inherited
84
+ self._shared_setter('alpha', alpha)
85
+
86
+ def set_edgecolor(self, color):
87
+ """
88
+ Set the edge color of the rectangle and the connectors.
89
+
90
+ Parameters
91
+ ----------
92
+ color : :mpltype:`color` or None
93
+ """
94
+ self._shared_setter('edgecolor', color)
95
+
96
+ def set_color(self, c):
97
+ """
98
+ Set the edgecolor of the rectangle and the connectors, and the
99
+ facecolor for the rectangle.
100
+
101
+ Parameters
102
+ ----------
103
+ c : :mpltype:`color`
104
+ """
105
+ self._shared_setter('edgecolor', c)
106
+ self._shared_setter('facecolor', c)
107
+
108
+ def set_linewidth(self, w):
109
+ """
110
+ Set the linewidth in points of the rectangle and the connectors.
111
+
112
+ Parameters
113
+ ----------
114
+ w : float or None
115
+ """
116
+ self._shared_setter('linewidth', w)
117
+
118
+ def set_linestyle(self, ls):
119
+ """
120
+ Set the linestyle of the rectangle and the connectors.
121
+
122
+ ========================================== =================
123
+ linestyle description
124
+ ========================================== =================
125
+ ``'-'`` or ``'solid'`` solid line
126
+ ``'--'`` or ``'dashed'`` dashed line
127
+ ``'-.'`` or ``'dashdot'`` dash-dotted line
128
+ ``':'`` or ``'dotted'`` dotted line
129
+ ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing
130
+ ========================================== =================
131
+
132
+ Alternatively a dash tuple of the following form can be provided::
133
+
134
+ (offset, onoffseq)
135
+
136
+ where ``onoffseq`` is an even length tuple of on and off ink in points.
137
+
138
+ Parameters
139
+ ----------
140
+ ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
141
+ The line style.
142
+ """
143
+ self._shared_setter('linestyle', ls)
144
+
145
+ def _bounds_from_inset_ax(self):
146
+ xlim = self._inset_ax.get_xlim()
147
+ ylim = self._inset_ax.get_ylim()
148
+ return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0])
149
+
150
+ def _update_connectors(self):
151
+ (x, y) = self._rectangle.get_xy()
152
+ width = self._rectangle.get_width()
153
+ height = self._rectangle.get_height()
154
+
155
+ existing_connectors = self._connectors or [None] * 4
156
+
157
+ # connect the inset_axes to the rectangle
158
+ for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)],
159
+ existing_connectors):
160
+ # inset_ax positions are in axes coordinates
161
+ # The 0, 1 values define the four edges if the inset_ax
162
+ # lower_left, upper_left, lower_right upper_right.
163
+ ex, ey = xy_inset_ax
164
+ if self.axes.xaxis.get_inverted():
165
+ ex = 1 - ex
166
+ if self.axes.yaxis.get_inverted():
167
+ ey = 1 - ey
168
+ xy_data = x + ex * width, y + ey * height
169
+ if existing is None:
170
+ # Create new connection patch with styles inherited from the
171
+ # parent artist.
172
+ p = ConnectionPatch(
173
+ xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes,
174
+ xyB=xy_data, coordsB=self.axes.transData,
175
+ arrowstyle="-",
176
+ edgecolor=self._edgecolor, alpha=self.get_alpha(),
177
+ linestyle=self._linestyle, linewidth=self._linewidth)
178
+ self._connectors.append(p)
179
+ else:
180
+ # Only update positioning of existing connection patch. We
181
+ # do not want to override any style settings made by the user.
182
+ existing.xy1 = xy_inset_ax
183
+ existing.xy2 = xy_data
184
+ existing.coords1 = self._inset_ax.transAxes
185
+ existing.coords2 = self.axes.transData
186
+
187
+ if existing is None:
188
+ # decide which two of the lines to keep visible....
189
+ pos = self._inset_ax.get_position()
190
+ bboxins = pos.transformed(self.get_figure(root=False).transSubfigure)
191
+ rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed(
192
+ self._rectangle.get_transform())
193
+ x0 = rectbbox.x0 < bboxins.x0
194
+ x1 = rectbbox.x1 < bboxins.x1
195
+ y0 = rectbbox.y0 < bboxins.y0
196
+ y1 = rectbbox.y1 < bboxins.y1
197
+ self._connectors[0].set_visible(x0 ^ y0)
198
+ self._connectors[1].set_visible(x0 == y1)
199
+ self._connectors[2].set_visible(x1 == y0)
200
+ self._connectors[3].set_visible(x1 ^ y1)
201
+
202
+ @property
203
+ def rectangle(self):
204
+ """`.Rectangle`: the indicator frame."""
205
+ return self._rectangle
206
+
207
+ @property
208
+ def connectors(self):
209
+ """
210
+ 4-tuple of `.patches.ConnectionPatch` or None
211
+ The four connector lines connecting to (lower_left, upper_left,
212
+ lower_right upper_right) corners of *inset_ax*. Two lines are
213
+ set with visibility to *False*, but the user can set the
214
+ visibility to True if the automatic choice is not deemed correct.
215
+ """
216
+ if self._inset_ax is None:
217
+ return
218
+
219
+ if self._auto_update_bounds:
220
+ self._rectangle.set_bounds(self._bounds_from_inset_ax())
221
+ self._update_connectors()
222
+ return tuple(self._connectors)
223
+
224
+ def draw(self, renderer):
225
+ # docstring inherited
226
+ conn_same_style = []
227
+
228
+ # Figure out which connectors have the same style as the box, so should
229
+ # be drawn as a single path.
230
+ for conn in self.connectors or []:
231
+ if conn.get_visible():
232
+ drawn = False
233
+ for s in _shared_properties:
234
+ if artist.getp(self._rectangle, s) != artist.getp(conn, s):
235
+ # Draw this connector by itself
236
+ conn.draw(renderer)
237
+ drawn = True
238
+ break
239
+
240
+ if not drawn:
241
+ # Connector has same style as box.
242
+ conn_same_style.append(conn)
243
+
244
+ if conn_same_style:
245
+ # Since at least one connector has the same style as the rectangle, draw
246
+ # them as a compound path.
247
+ artists = [self._rectangle] + conn_same_style
248
+ paths = [a.get_transform().transform_path(a.get_path()) for a in artists]
249
+ path = Path.make_compound_path(*paths)
250
+
251
+ # Create a temporary patch to draw the path.
252
+ p = PathPatch(path)
253
+ p.update_from(self._rectangle)
254
+ p.set_transform(transforms.IdentityTransform())
255
+ p.draw(renderer)
256
+
257
+ return
258
+
259
+ # Just draw the rectangle
260
+ self._rectangle.draw(renderer)
261
+
262
+ @_api.deprecated(
263
+ '3.10',
264
+ message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single '
265
+ 'InsetIndicator artist with a rectangle property and a connectors '
266
+ 'property. From 3.12 it will no longer be possible to unpack the '
267
+ 'return value into two elements.'))
268
+ def __getitem__(self, key):
269
+ return [self._rectangle, self.connectors][key]
lib/python3.12/site-packages/matplotlib/layout_engine.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Classes to layout elements in a `.Figure`.
3
+
4
+ Figures have a ``layout_engine`` property that holds a subclass of
5
+ `~.LayoutEngine` defined here (or *None* for no layout). At draw time
6
+ ``figure.get_layout_engine().execute()`` is called, the goal of which is
7
+ usually to rearrange Axes on the figure to produce a pleasing layout. This is
8
+ like a ``draw`` callback but with two differences. First, when printing we
9
+ disable the layout engine for the final draw. Second, it is useful to know the
10
+ layout engine while the figure is being created. In particular, colorbars are
11
+ made differently with different layout engines (for historical reasons).
12
+
13
+ Matplotlib has two built-in layout engines:
14
+
15
+ - `.TightLayoutEngine` was the first layout engine added to Matplotlib.
16
+ See also :ref:`tight_layout_guide`.
17
+ - `.ConstrainedLayoutEngine` is more modern and generally gives better results.
18
+ See also :ref:`constrainedlayout_guide`.
19
+
20
+ Third parties can create their own layout engine by subclassing `.LayoutEngine`.
21
+ """
22
+
23
+ from contextlib import nullcontext
24
+
25
+ import matplotlib as mpl
26
+
27
+ from matplotlib._constrained_layout import do_constrained_layout
28
+ from matplotlib._tight_layout import (get_subplotspec_list,
29
+ get_tight_layout_figure)
30
+
31
+
32
+ class LayoutEngine:
33
+ """
34
+ Base class for Matplotlib layout engines.
35
+
36
+ A layout engine can be passed to a figure at instantiation or at any time
37
+ with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the
38
+ layout engine ``execute`` function is called at draw time by
39
+ `~.figure.Figure.draw`, providing a special draw-time hook.
40
+
41
+ .. note::
42
+
43
+ However, note that layout engines affect the creation of colorbars, so
44
+ `~.figure.Figure.set_layout_engine` should be called before any
45
+ colorbars are created.
46
+
47
+ Currently, there are two properties of `LayoutEngine` classes that are
48
+ consulted while manipulating the figure:
49
+
50
+ - ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the
51
+ axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or
52
+ not (see `.colorbar.make_axes`);
53
+ - ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being
54
+ run if it is not compatible with the layout engine.
55
+
56
+ To implement a custom `LayoutEngine`:
57
+
58
+ 1. override ``_adjust_compatible`` and ``_colorbar_gridspec``
59
+ 2. override `LayoutEngine.set` to update *self._params*
60
+ 3. override `LayoutEngine.execute` with your implementation
61
+
62
+ """
63
+ # override these in subclass
64
+ _adjust_compatible = None
65
+ _colorbar_gridspec = None
66
+
67
+ def __init__(self, **kwargs):
68
+ super().__init__(**kwargs)
69
+ self._params = {}
70
+
71
+ def set(self, **kwargs):
72
+ """
73
+ Set the parameters for the layout engine.
74
+ """
75
+ raise NotImplementedError
76
+
77
+ @property
78
+ def colorbar_gridspec(self):
79
+ """
80
+ Return a boolean if the layout engine creates colorbars using a
81
+ gridspec.
82
+ """
83
+ if self._colorbar_gridspec is None:
84
+ raise NotImplementedError
85
+ return self._colorbar_gridspec
86
+
87
+ @property
88
+ def adjust_compatible(self):
89
+ """
90
+ Return a boolean if the layout engine is compatible with
91
+ `~.Figure.subplots_adjust`.
92
+ """
93
+ if self._adjust_compatible is None:
94
+ raise NotImplementedError
95
+ return self._adjust_compatible
96
+
97
+ def get(self):
98
+ """
99
+ Return copy of the parameters for the layout engine.
100
+ """
101
+ return dict(self._params)
102
+
103
+ def execute(self, fig):
104
+ """
105
+ Execute the layout on the figure given by *fig*.
106
+ """
107
+ # subclasses must implement this.
108
+ raise NotImplementedError
109
+
110
+
111
+ class PlaceHolderLayoutEngine(LayoutEngine):
112
+ """
113
+ This layout engine does not adjust the figure layout at all.
114
+
115
+ The purpose of this `.LayoutEngine` is to act as a placeholder when the user removes
116
+ a layout engine to ensure an incompatible `.LayoutEngine` cannot be set later.
117
+
118
+ Parameters
119
+ ----------
120
+ adjust_compatible, colorbar_gridspec : bool
121
+ Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever
122
+ layout engine it is replacing.
123
+
124
+ """
125
+ def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs):
126
+ self._adjust_compatible = adjust_compatible
127
+ self._colorbar_gridspec = colorbar_gridspec
128
+ super().__init__(**kwargs)
129
+
130
+ def execute(self, fig):
131
+ """
132
+ Do nothing.
133
+ """
134
+ return
135
+
136
+
137
+ class TightLayoutEngine(LayoutEngine):
138
+ """
139
+ Implements the ``tight_layout`` geometry management. See
140
+ :ref:`tight_layout_guide` for details.
141
+ """
142
+ _adjust_compatible = True
143
+ _colorbar_gridspec = True
144
+
145
+ def __init__(self, *, pad=1.08, h_pad=None, w_pad=None,
146
+ rect=(0, 0, 1, 1), **kwargs):
147
+ """
148
+ Initialize tight_layout engine.
149
+
150
+ Parameters
151
+ ----------
152
+ pad : float, default: 1.08
153
+ Padding between the figure edge and the edges of subplots, as a
154
+ fraction of the font size.
155
+ h_pad, w_pad : float
156
+ Padding (height/width) between edges of adjacent subplots.
157
+ Defaults to *pad*.
158
+ rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1).
159
+ rectangle in normalized figure coordinates that the subplots
160
+ (including labels) will fit into.
161
+ """
162
+ super().__init__(**kwargs)
163
+ for td in ['pad', 'h_pad', 'w_pad', 'rect']:
164
+ # initialize these in case None is passed in above:
165
+ self._params[td] = None
166
+ self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
167
+
168
+ def execute(self, fig):
169
+ """
170
+ Execute tight_layout.
171
+
172
+ This decides the subplot parameters given the padding that
173
+ will allow the Axes labels to not be covered by other labels
174
+ and Axes.
175
+
176
+ Parameters
177
+ ----------
178
+ fig : `.Figure` to perform layout on.
179
+
180
+ See Also
181
+ --------
182
+ .figure.Figure.tight_layout
183
+ .pyplot.tight_layout
184
+ """
185
+ info = self._params
186
+ renderer = fig._get_renderer()
187
+ with getattr(renderer, "_draw_disabled", nullcontext)():
188
+ kwargs = get_tight_layout_figure(
189
+ fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
190
+ pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
191
+ rect=info['rect'])
192
+ if kwargs:
193
+ fig.subplots_adjust(**kwargs)
194
+
195
+ def set(self, *, pad=None, w_pad=None, h_pad=None, rect=None):
196
+ """
197
+ Set the pads for tight_layout.
198
+
199
+ Parameters
200
+ ----------
201
+ pad : float
202
+ Padding between the figure edge and the edges of subplots, as a
203
+ fraction of the font size.
204
+ w_pad, h_pad : float
205
+ Padding (width/height) between edges of adjacent subplots.
206
+ Defaults to *pad*.
207
+ rect : tuple (left, bottom, right, top)
208
+ rectangle in normalized figure coordinates that the subplots
209
+ (including labels) will fit into.
210
+ """
211
+ for td in self.set.__kwdefaults__:
212
+ if locals()[td] is not None:
213
+ self._params[td] = locals()[td]
214
+
215
+
216
+ class ConstrainedLayoutEngine(LayoutEngine):
217
+ """
218
+ Implements the ``constrained_layout`` geometry management. See
219
+ :ref:`constrainedlayout_guide` for details.
220
+ """
221
+
222
+ _adjust_compatible = False
223
+ _colorbar_gridspec = False
224
+
225
+ def __init__(self, *, h_pad=None, w_pad=None,
226
+ hspace=None, wspace=None, rect=(0, 0, 1, 1),
227
+ compress=False, **kwargs):
228
+ """
229
+ Initialize ``constrained_layout`` settings.
230
+
231
+ Parameters
232
+ ----------
233
+ h_pad, w_pad : float
234
+ Padding around the Axes elements in inches.
235
+ Default to :rc:`figure.constrained_layout.h_pad` and
236
+ :rc:`figure.constrained_layout.w_pad`.
237
+ hspace, wspace : float
238
+ Fraction of the figure to dedicate to space between the
239
+ axes. These are evenly spread between the gaps between the Axes.
240
+ A value of 0.2 for a three-column layout would have a space
241
+ of 0.1 of the figure width between each column.
242
+ If h/wspace < h/w_pad, then the pads are used instead.
243
+ Default to :rc:`figure.constrained_layout.hspace` and
244
+ :rc:`figure.constrained_layout.wspace`.
245
+ rect : tuple of 4 floats
246
+ Rectangle in figure coordinates to perform constrained layout in
247
+ (left, bottom, width, height), each from 0-1.
248
+ compress : bool
249
+ Whether to shift Axes so that white space in between them is
250
+ removed. This is useful for simple grids of fixed-aspect Axes (e.g.
251
+ a grid of images). See :ref:`compressed_layout`.
252
+ """
253
+ super().__init__(**kwargs)
254
+ # set the defaults:
255
+ self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'],
256
+ h_pad=mpl.rcParams['figure.constrained_layout.h_pad'],
257
+ wspace=mpl.rcParams['figure.constrained_layout.wspace'],
258
+ hspace=mpl.rcParams['figure.constrained_layout.hspace'],
259
+ rect=(0, 0, 1, 1))
260
+ # set anything that was passed in (None will be ignored):
261
+ self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace,
262
+ rect=rect)
263
+ self._compress = compress
264
+
265
+ def execute(self, fig):
266
+ """
267
+ Perform constrained_layout and move and resize Axes accordingly.
268
+
269
+ Parameters
270
+ ----------
271
+ fig : `.Figure` to perform layout on.
272
+ """
273
+ width, height = fig.get_size_inches()
274
+ # pads are relative to the current state of the figure...
275
+ w_pad = self._params['w_pad'] / width
276
+ h_pad = self._params['h_pad'] / height
277
+
278
+ return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
279
+ wspace=self._params['wspace'],
280
+ hspace=self._params['hspace'],
281
+ rect=self._params['rect'],
282
+ compress=self._compress)
283
+
284
+ def set(self, *, h_pad=None, w_pad=None,
285
+ hspace=None, wspace=None, rect=None):
286
+ """
287
+ Set the pads for constrained_layout.
288
+
289
+ Parameters
290
+ ----------
291
+ h_pad, w_pad : float
292
+ Padding around the Axes elements in inches.
293
+ Default to :rc:`figure.constrained_layout.h_pad` and
294
+ :rc:`figure.constrained_layout.w_pad`.
295
+ hspace, wspace : float
296
+ Fraction of the figure to dedicate to space between the
297
+ axes. These are evenly spread between the gaps between the Axes.
298
+ A value of 0.2 for a three-column layout would have a space
299
+ of 0.1 of the figure width between each column.
300
+ If h/wspace < h/w_pad, then the pads are used instead.
301
+ Default to :rc:`figure.constrained_layout.hspace` and
302
+ :rc:`figure.constrained_layout.wspace`.
303
+ rect : tuple of 4 floats
304
+ Rectangle in figure coordinates to perform constrained layout in
305
+ (left, bottom, width, height), each from 0-1.
306
+ """
307
+ for td in self.set.__kwdefaults__:
308
+ if locals()[td] is not None:
309
+ self._params[td] = locals()[td]
lib/python3.12/site-packages/matplotlib/legend.py ADDED
@@ -0,0 +1,1370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ The legend module defines the Legend class, which is responsible for
3
+ drawing legends associated with Axes and/or figures.
4
+
5
+ .. important::
6
+
7
+ It is unlikely that you would ever create a Legend instance manually.
8
+ Most users would normally create a legend via the `~.Axes.legend`
9
+ function. For more details on legends there is also a :ref:`legend guide
10
+ <legend_guide>`.
11
+
12
+ The `Legend` class is a container of legend handles and legend texts.
13
+
14
+ The legend handler map specifies how to create legend handles from artists
15
+ (lines, patches, etc.) in the Axes or figures. Default legend handlers are
16
+ defined in the :mod:`~matplotlib.legend_handler` module. While not all artist
17
+ types are covered by the default legend handlers, custom legend handlers can be
18
+ defined to support arbitrary objects.
19
+
20
+ See the :ref`<legend_guide>` for more
21
+ information.
22
+ """
23
+
24
+ import itertools
25
+ import logging
26
+ import numbers
27
+ import time
28
+
29
+ import numpy as np
30
+
31
+ import matplotlib as mpl
32
+ from matplotlib import _api, _docstring, cbook, colors, offsetbox
33
+ from matplotlib.artist import Artist, allow_rasterization
34
+ from matplotlib.cbook import silent_list
35
+ from matplotlib.font_manager import FontProperties
36
+ from matplotlib.lines import Line2D
37
+ from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch,
38
+ StepPatch)
39
+ from matplotlib.collections import (
40
+ Collection, CircleCollection, LineCollection, PathCollection,
41
+ PolyCollection, RegularPolyCollection)
42
+ from matplotlib.text import Text
43
+ from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
44
+ from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
45
+ from matplotlib.offsetbox import (
46
+ AnchoredOffsetbox, DraggableOffsetBox,
47
+ HPacker, VPacker,
48
+ DrawingArea, TextArea,
49
+ )
50
+ from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
51
+ from . import legend_handler
52
+
53
+
54
+ class DraggableLegend(DraggableOffsetBox):
55
+ def __init__(self, legend, use_blit=False, update="loc"):
56
+ """
57
+ Wrapper around a `.Legend` to support mouse dragging.
58
+
59
+ Parameters
60
+ ----------
61
+ legend : `.Legend`
62
+ The `.Legend` instance to wrap.
63
+ use_blit : bool, optional
64
+ Use blitting for faster image composition. For details see
65
+ :ref:`func-animation`.
66
+ update : {'loc', 'bbox'}, optional
67
+ If "loc", update the *loc* parameter of the legend upon finalizing.
68
+ If "bbox", update the *bbox_to_anchor* parameter.
69
+ """
70
+ self.legend = legend
71
+
72
+ _api.check_in_list(["loc", "bbox"], update=update)
73
+ self._update = update
74
+
75
+ super().__init__(legend, legend._legend_box, use_blit=use_blit)
76
+
77
+ def finalize_offset(self):
78
+ if self._update == "loc":
79
+ self._update_loc(self.get_loc_in_canvas())
80
+ elif self._update == "bbox":
81
+ self._update_bbox_to_anchor(self.get_loc_in_canvas())
82
+
83
+ def _update_loc(self, loc_in_canvas):
84
+ bbox = self.legend.get_bbox_to_anchor()
85
+ # if bbox has zero width or height, the transformation is
86
+ # ill-defined. Fall back to the default bbox_to_anchor.
87
+ if bbox.width == 0 or bbox.height == 0:
88
+ self.legend.set_bbox_to_anchor(None)
89
+ bbox = self.legend.get_bbox_to_anchor()
90
+ _bbox_transform = BboxTransformFrom(bbox)
91
+ self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas))
92
+
93
+ def _update_bbox_to_anchor(self, loc_in_canvas):
94
+ loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas)
95
+ self.legend.set_bbox_to_anchor(loc_in_bbox)
96
+
97
+
98
+ _legend_kw_doc_base = """
99
+ bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
100
+ Box that is used to position the legend in conjunction with *loc*.
101
+ Defaults to ``axes.bbox`` (if called as a method to `.Axes.legend`) or
102
+ ``figure.bbox`` (if ``figure.legend``). This argument allows arbitrary
103
+ placement of the legend.
104
+
105
+ Bbox coordinates are interpreted in the coordinate system given by
106
+ *bbox_transform*, with the default transform
107
+ Axes or Figure coordinates, depending on which ``legend`` is called.
108
+
109
+ If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
110
+ ``(x, y, width, height)`` that the legend is placed in.
111
+ To put the legend in the best location in the bottom right
112
+ quadrant of the Axes (or figure)::
113
+
114
+ loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
115
+
116
+ A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
117
+ x, y. For example, to put the legend's upper right-hand corner in the
118
+ center of the Axes (or figure) the following keywords can be used::
119
+
120
+ loc='upper right', bbox_to_anchor=(0.5, 0.5)
121
+
122
+ ncols : int, default: 1
123
+ The number of columns that the legend has.
124
+
125
+ For backward compatibility, the spelling *ncol* is also supported
126
+ but it is discouraged. If both are given, *ncols* takes precedence.
127
+
128
+ prop : None or `~matplotlib.font_manager.FontProperties` or dict
129
+ The font properties of the legend. If None (default), the current
130
+ :data:`matplotlib.rcParams` will be used.
131
+
132
+ fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
133
+ 'x-large', 'xx-large'}
134
+ The font size of the legend. If the value is numeric the size will be the
135
+ absolute font size in points. String values are relative to the current
136
+ default font size. This argument is only used if *prop* is not specified.
137
+
138
+ labelcolor : str or list, default: :rc:`legend.labelcolor`
139
+ The color of the text in the legend. Either a valid color string
140
+ (for example, 'red'), or a list of color strings. The labelcolor can
141
+ also be made to match the color of the line or marker using 'linecolor',
142
+ 'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec').
143
+
144
+ Labelcolor can be set globally using :rc:`legend.labelcolor`. If None,
145
+ use :rc:`text.color`.
146
+
147
+ numpoints : int, default: :rc:`legend.numpoints`
148
+ The number of marker points in the legend when creating a legend
149
+ entry for a `.Line2D` (line).
150
+
151
+ scatterpoints : int, default: :rc:`legend.scatterpoints`
152
+ The number of marker points in the legend when creating
153
+ a legend entry for a `.PathCollection` (scatter plot).
154
+
155
+ scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]``
156
+ The vertical offset (relative to the font size) for the markers
157
+ created for a scatter plot legend entry. 0.0 is at the base the
158
+ legend text, and 1.0 is at the top. To draw all markers at the
159
+ same height, set to ``[0.5]``.
160
+
161
+ markerscale : float, default: :rc:`legend.markerscale`
162
+ The relative size of legend markers compared to the originally drawn ones.
163
+
164
+ markerfirst : bool, default: True
165
+ If *True*, legend marker is placed to the left of the legend label.
166
+ If *False*, legend marker is placed to the right of the legend label.
167
+
168
+ reverse : bool, default: False
169
+ If *True*, the legend labels are displayed in reverse order from the input.
170
+ If *False*, the legend labels are displayed in the same order as the input.
171
+
172
+ .. versionadded:: 3.7
173
+
174
+ frameon : bool, default: :rc:`legend.frameon`
175
+ Whether the legend should be drawn on a patch (frame).
176
+
177
+ fancybox : bool, default: :rc:`legend.fancybox`
178
+ Whether round edges should be enabled around the `.FancyBboxPatch` which
179
+ makes up the legend's background.
180
+
181
+ shadow : None, bool or dict, default: :rc:`legend.shadow`
182
+ Whether to draw a shadow behind the legend.
183
+ The shadow can be configured using `.Patch` keywords.
184
+ Customization via :rc:`legend.shadow` is currently not supported.
185
+
186
+ framealpha : float, default: :rc:`legend.framealpha`
187
+ The alpha transparency of the legend's background.
188
+ If *shadow* is activated and *framealpha* is ``None``, the default value is
189
+ ignored.
190
+
191
+ facecolor : "inherit" or color, default: :rc:`legend.facecolor`
192
+ The legend's background color.
193
+ If ``"inherit"``, use :rc:`axes.facecolor`.
194
+
195
+ edgecolor : "inherit" or color, default: :rc:`legend.edgecolor`
196
+ The legend's background patch edge color.
197
+ If ``"inherit"``, use :rc:`axes.edgecolor`.
198
+
199
+ mode : {"expand", None}
200
+ If *mode* is set to ``"expand"`` the legend will be horizontally
201
+ expanded to fill the Axes area (or *bbox_to_anchor* if defines
202
+ the legend's size).
203
+
204
+ bbox_transform : None or `~matplotlib.transforms.Transform`
205
+ The transform for the bounding box (*bbox_to_anchor*). For a value
206
+ of ``None`` (default) the Axes'
207
+ :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
208
+
209
+ title : str or None
210
+ The legend's title. Default is no title (``None``).
211
+
212
+ title_fontproperties : None or `~matplotlib.font_manager.FontProperties` or dict
213
+ The font properties of the legend's title. If None (default), the
214
+ *title_fontsize* argument will be used if present; if *title_fontsize* is
215
+ also None, the current :rc:`legend.title_fontsize` will be used.
216
+
217
+ title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
218
+ 'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize`
219
+ The font size of the legend's title.
220
+ Note: This cannot be combined with *title_fontproperties*. If you want
221
+ to set the fontsize alongside other font properties, use the *size*
222
+ parameter in *title_fontproperties*.
223
+
224
+ alignment : {'center', 'left', 'right'}, default: 'center'
225
+ The alignment of the legend title and the box of entries. The entries
226
+ are aligned as a single block, so that markers always lined up.
227
+
228
+ borderpad : float, default: :rc:`legend.borderpad`
229
+ The fractional whitespace inside the legend border, in font-size units.
230
+
231
+ labelspacing : float, default: :rc:`legend.labelspacing`
232
+ The vertical space between the legend entries, in font-size units.
233
+
234
+ handlelength : float, default: :rc:`legend.handlelength`
235
+ The length of the legend handles, in font-size units.
236
+
237
+ handleheight : float, default: :rc:`legend.handleheight`
238
+ The height of the legend handles, in font-size units.
239
+
240
+ handletextpad : float, default: :rc:`legend.handletextpad`
241
+ The pad between the legend handle and text, in font-size units.
242
+
243
+ borderaxespad : float, default: :rc:`legend.borderaxespad`
244
+ The pad between the Axes and legend border, in font-size units.
245
+
246
+ columnspacing : float, default: :rc:`legend.columnspacing`
247
+ The spacing between columns, in font-size units.
248
+
249
+ handler_map : dict or None
250
+ The custom dictionary mapping instances or types to a legend
251
+ handler. This *handler_map* updates the default handler map
252
+ found at `matplotlib.legend.Legend.get_legend_handler_map`.
253
+
254
+ draggable : bool, default: False
255
+ Whether the legend can be dragged with the mouse.
256
+ """
257
+
258
+ _loc_doc_base = """
259
+ loc : str or pair of floats, default: {default}
260
+ The location of the legend.
261
+
262
+ The strings ``'upper left'``, ``'upper right'``, ``'lower left'``,
263
+ ``'lower right'`` place the legend at the corresponding corner of the
264
+ {parent}.
265
+
266
+ The strings ``'upper center'``, ``'lower center'``, ``'center left'``,
267
+ ``'center right'`` place the legend at the center of the corresponding edge
268
+ of the {parent}.
269
+
270
+ The string ``'center'`` places the legend at the center of the {parent}.
271
+ {best}
272
+ The location can also be a 2-tuple giving the coordinates of the lower-left
273
+ corner of the legend in {parent} coordinates (in which case *bbox_to_anchor*
274
+ will be ignored).
275
+
276
+ For back-compatibility, ``'center right'`` (but no other location) can also
277
+ be spelled ``'right'``, and each "string" location can also be given as a
278
+ numeric value:
279
+
280
+ ================== =============
281
+ Location String Location Code
282
+ ================== =============
283
+ 'best' (Axes only) 0
284
+ 'upper right' 1
285
+ 'upper left' 2
286
+ 'lower left' 3
287
+ 'lower right' 4
288
+ 'right' 5
289
+ 'center left' 6
290
+ 'center right' 7
291
+ 'lower center' 8
292
+ 'upper center' 9
293
+ 'center' 10
294
+ ================== =============
295
+ {outside}"""
296
+
297
+ _loc_doc_best = """
298
+ The string ``'best'`` places the legend at the location, among the nine
299
+ locations defined so far, with the minimum overlap with other drawn
300
+ artists. This option can be quite slow for plots with large amounts of
301
+ data; your plotting speed may benefit from providing a specific location.
302
+ """
303
+
304
+ _legend_kw_axes_st = (
305
+ _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`',
306
+ best=_loc_doc_best, outside='') +
307
+ _legend_kw_doc_base)
308
+ _docstring.interpd.register(_legend_kw_axes=_legend_kw_axes_st)
309
+
310
+ _outside_doc = """
311
+ If a figure is using the constrained layout manager, the string codes
312
+ of the *loc* keyword argument can get better layout behaviour using the
313
+ prefix 'outside'. There is ambiguity at the corners, so 'outside
314
+ upper right' will make space for the legend above the rest of the
315
+ axes in the layout, and 'outside right upper' will make space on the
316
+ right side of the layout. In addition to the values of *loc*
317
+ listed above, we have 'outside right upper', 'outside right lower',
318
+ 'outside left upper', and 'outside left lower'. See
319
+ :ref:`legend_guide` for more details.
320
+ """
321
+
322
+ _legend_kw_figure_st = (
323
+ _loc_doc_base.format(parent='figure', default="'upper right'",
324
+ best='', outside=_outside_doc) +
325
+ _legend_kw_doc_base)
326
+ _docstring.interpd.register(_legend_kw_figure=_legend_kw_figure_st)
327
+
328
+ _legend_kw_both_st = (
329
+ _loc_doc_base.format(parent='axes/figure',
330
+ default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
331
+ best=_loc_doc_best, outside=_outside_doc) +
332
+ _legend_kw_doc_base)
333
+ _docstring.interpd.register(_legend_kw_doc=_legend_kw_both_st)
334
+
335
+ _legend_kw_set_loc_st = (
336
+ _loc_doc_base.format(parent='axes/figure',
337
+ default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
338
+ best=_loc_doc_best, outside=_outside_doc))
339
+ _docstring.interpd.register(_legend_kw_set_loc_doc=_legend_kw_set_loc_st)
340
+
341
+
342
+ class Legend(Artist):
343
+ """
344
+ Place a legend on the figure/axes.
345
+ """
346
+
347
+ # 'best' is only implemented for Axes legends
348
+ codes = {'best': 0, **AnchoredOffsetbox.codes}
349
+ zorder = 5
350
+
351
+ def __str__(self):
352
+ return "Legend"
353
+
354
+ @_docstring.interpd
355
+ def __init__(
356
+ self, parent, handles, labels,
357
+ *,
358
+ loc=None,
359
+ numpoints=None, # number of points in the legend line
360
+ markerscale=None, # relative size of legend markers vs. original
361
+ markerfirst=True, # left/right ordering of legend marker and label
362
+ reverse=False, # reverse ordering of legend marker and label
363
+ scatterpoints=None, # number of scatter points
364
+ scatteryoffsets=None,
365
+ prop=None, # properties for the legend texts
366
+ fontsize=None, # keyword to set font size directly
367
+ labelcolor=None, # keyword to set the text color
368
+
369
+ # spacing & pad defined as a fraction of the font-size
370
+ borderpad=None, # whitespace inside the legend border
371
+ labelspacing=None, # vertical space between the legend entries
372
+ handlelength=None, # length of the legend handles
373
+ handleheight=None, # height of the legend handles
374
+ handletextpad=None, # pad between the legend handle and text
375
+ borderaxespad=None, # pad between the Axes and legend border
376
+ columnspacing=None, # spacing between columns
377
+
378
+ ncols=1, # number of columns
379
+ mode=None, # horizontal distribution of columns: None or "expand"
380
+
381
+ fancybox=None, # True: fancy box, False: rounded box, None: rcParam
382
+ shadow=None,
383
+ title=None, # legend title
384
+ title_fontsize=None, # legend title font size
385
+ framealpha=None, # set frame alpha
386
+ edgecolor=None, # frame patch edgecolor
387
+ facecolor=None, # frame patch facecolor
388
+
389
+ bbox_to_anchor=None, # bbox to which the legend will be anchored
390
+ bbox_transform=None, # transform for the bbox
391
+ frameon=None, # draw frame
392
+ handler_map=None,
393
+ title_fontproperties=None, # properties for the legend title
394
+ alignment="center", # control the alignment within the legend box
395
+ ncol=1, # synonym for ncols (backward compatibility)
396
+ draggable=False # whether the legend can be dragged with the mouse
397
+ ):
398
+ """
399
+ Parameters
400
+ ----------
401
+ parent : `~matplotlib.axes.Axes` or `.Figure`
402
+ The artist that contains the legend.
403
+
404
+ handles : list of (`.Artist` or tuple of `.Artist`)
405
+ A list of Artists (lines, patches) to be added to the legend.
406
+
407
+ labels : list of str
408
+ A list of labels to show next to the artists. The length of handles
409
+ and labels should be the same. If they are not, they are truncated
410
+ to the length of the shorter list.
411
+
412
+ Other Parameters
413
+ ----------------
414
+ %(_legend_kw_doc)s
415
+
416
+ Attributes
417
+ ----------
418
+ legend_handles
419
+ List of `.Artist` objects added as legend entries.
420
+
421
+ .. versionadded:: 3.7
422
+ """
423
+ # local import only to avoid circularity
424
+ from matplotlib.axes import Axes
425
+ from matplotlib.figure import FigureBase
426
+
427
+ super().__init__()
428
+
429
+ if prop is None:
430
+ self.prop = FontProperties(size=mpl._val_or_rc(fontsize, "legend.fontsize"))
431
+ else:
432
+ self.prop = FontProperties._from_any(prop)
433
+ if isinstance(prop, dict) and "size" not in prop:
434
+ self.prop.set_size(mpl.rcParams["legend.fontsize"])
435
+
436
+ self._fontsize = self.prop.get_size_in_points()
437
+
438
+ self.texts = []
439
+ self.legend_handles = []
440
+ self._legend_title_box = None
441
+
442
+ #: A dictionary with the extra handler mappings for this Legend
443
+ #: instance.
444
+ self._custom_handler_map = handler_map
445
+
446
+ self.numpoints = mpl._val_or_rc(numpoints, 'legend.numpoints')
447
+ self.markerscale = mpl._val_or_rc(markerscale, 'legend.markerscale')
448
+ self.scatterpoints = mpl._val_or_rc(scatterpoints, 'legend.scatterpoints')
449
+ self.borderpad = mpl._val_or_rc(borderpad, 'legend.borderpad')
450
+ self.labelspacing = mpl._val_or_rc(labelspacing, 'legend.labelspacing')
451
+ self.handlelength = mpl._val_or_rc(handlelength, 'legend.handlelength')
452
+ self.handleheight = mpl._val_or_rc(handleheight, 'legend.handleheight')
453
+ self.handletextpad = mpl._val_or_rc(handletextpad, 'legend.handletextpad')
454
+ self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad')
455
+ self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing')
456
+ self.shadow = mpl._val_or_rc(shadow, 'legend.shadow')
457
+
458
+ if reverse:
459
+ labels = [*reversed(labels)]
460
+ handles = [*reversed(handles)]
461
+
462
+ if len(handles) < 2:
463
+ ncols = 1
464
+ self._ncols = ncols if ncols != 1 else ncol
465
+
466
+ if self.numpoints <= 0:
467
+ raise ValueError("numpoints must be > 0; it was %d" % numpoints)
468
+
469
+ # introduce y-offset for handles of the scatter plot
470
+ if scatteryoffsets is None:
471
+ self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
472
+ else:
473
+ self._scatteryoffsets = np.asarray(scatteryoffsets)
474
+ reps = self.scatterpoints // len(self._scatteryoffsets) + 1
475
+ self._scatteryoffsets = np.tile(self._scatteryoffsets,
476
+ reps)[:self.scatterpoints]
477
+
478
+ # _legend_box is a VPacker instance that contains all
479
+ # legend items and will be initialized from _init_legend_box()
480
+ # method.
481
+ self._legend_box = None
482
+
483
+ if isinstance(parent, Axes):
484
+ self.isaxes = True
485
+ self.axes = parent
486
+ self.set_figure(parent.get_figure(root=False))
487
+ elif isinstance(parent, FigureBase):
488
+ self.isaxes = False
489
+ self.set_figure(parent)
490
+ else:
491
+ raise TypeError(
492
+ "Legend needs either Axes or FigureBase as parent"
493
+ )
494
+ self.parent = parent
495
+
496
+ self._mode = mode
497
+ self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
498
+
499
+ # Figure out if self.shadow is valid
500
+ # If shadow was None, rcParams loads False
501
+ # So it shouldn't be None here
502
+
503
+ self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets
504
+ if isinstance(self.shadow, dict):
505
+ self._shadow_props.update(self.shadow)
506
+ self.shadow = True
507
+ elif self.shadow in (0, 1, True, False):
508
+ self.shadow = bool(self.shadow)
509
+ else:
510
+ raise ValueError(
511
+ 'Legend shadow must be a dict or bool, not '
512
+ f'{self.shadow!r} of type {type(self.shadow)}.'
513
+ )
514
+
515
+ # We use FancyBboxPatch to draw a legend frame. The location
516
+ # and size of the box will be updated during the drawing time.
517
+
518
+ facecolor = mpl._val_or_rc(facecolor, "legend.facecolor")
519
+ if facecolor == 'inherit':
520
+ facecolor = mpl.rcParams["axes.facecolor"]
521
+
522
+ edgecolor = mpl._val_or_rc(edgecolor, "legend.edgecolor")
523
+ if edgecolor == 'inherit':
524
+ edgecolor = mpl.rcParams["axes.edgecolor"]
525
+
526
+ fancybox = mpl._val_or_rc(fancybox, "legend.fancybox")
527
+
528
+ self.legendPatch = FancyBboxPatch(
529
+ xy=(0, 0), width=1, height=1,
530
+ facecolor=facecolor, edgecolor=edgecolor,
531
+ # If shadow is used, default to alpha=1 (#8943).
532
+ alpha=(framealpha if framealpha is not None
533
+ else 1 if shadow
534
+ else mpl.rcParams["legend.framealpha"]),
535
+ # The width and height of the legendPatch will be set (in draw())
536
+ # to the length that includes the padding. Thus we set pad=0 here.
537
+ boxstyle=("round,pad=0,rounding_size=0.2" if fancybox
538
+ else "square,pad=0"),
539
+ mutation_scale=self._fontsize,
540
+ snap=True,
541
+ visible=mpl._val_or_rc(frameon, "legend.frameon")
542
+ )
543
+ self._set_artist_props(self.legendPatch)
544
+
545
+ _api.check_in_list(["center", "left", "right"], alignment=alignment)
546
+ self._alignment = alignment
547
+
548
+ # init with null renderer
549
+ self._init_legend_box(handles, labels, markerfirst)
550
+
551
+ # Set legend location
552
+ self.set_loc(loc)
553
+
554
+ # figure out title font properties:
555
+ if title_fontsize is not None and title_fontproperties is not None:
556
+ raise ValueError(
557
+ "title_fontsize and title_fontproperties can't be specified "
558
+ "at the same time. Only use one of them. ")
559
+ title_prop_fp = FontProperties._from_any(title_fontproperties)
560
+ if isinstance(title_fontproperties, dict):
561
+ if "size" not in title_fontproperties:
562
+ title_fontsize = mpl.rcParams["legend.title_fontsize"]
563
+ title_prop_fp.set_size(title_fontsize)
564
+ elif title_fontsize is not None:
565
+ title_prop_fp.set_size(title_fontsize)
566
+ elif not isinstance(title_fontproperties, FontProperties):
567
+ title_fontsize = mpl.rcParams["legend.title_fontsize"]
568
+ title_prop_fp.set_size(title_fontsize)
569
+
570
+ self.set_title(title, prop=title_prop_fp)
571
+
572
+ self._draggable = None
573
+ self.set_draggable(state=draggable)
574
+
575
+ # set the text color
576
+
577
+ color_getters = { # getter function depends on line or patch
578
+ 'linecolor': ['get_color', 'get_facecolor'],
579
+ 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
580
+ 'mfc': ['get_markerfacecolor', 'get_facecolor'],
581
+ 'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
582
+ 'mec': ['get_markeredgecolor', 'get_edgecolor'],
583
+ }
584
+ labelcolor = mpl._val_or_rc(labelcolor, 'legend.labelcolor')
585
+ if labelcolor is None:
586
+ labelcolor = mpl.rcParams['text.color']
587
+ if isinstance(labelcolor, str) and labelcolor in color_getters:
588
+ getter_names = color_getters[labelcolor]
589
+ for handle, text in zip(self.legend_handles, self.texts):
590
+ try:
591
+ if handle.get_array() is not None:
592
+ continue
593
+ except AttributeError:
594
+ pass
595
+ for getter_name in getter_names:
596
+ try:
597
+ color = getattr(handle, getter_name)()
598
+ if isinstance(color, np.ndarray):
599
+ if (
600
+ color.shape[0] == 1
601
+ or np.isclose(color, color[0]).all()
602
+ ):
603
+ text.set_color(color[0])
604
+ else:
605
+ pass
606
+ else:
607
+ text.set_color(color)
608
+ break
609
+ except AttributeError:
610
+ pass
611
+ elif cbook._str_equal(labelcolor, 'none'):
612
+ for text in self.texts:
613
+ text.set_color(labelcolor)
614
+ elif np.iterable(labelcolor):
615
+ for text, color in zip(self.texts,
616
+ itertools.cycle(
617
+ colors.to_rgba_array(labelcolor))):
618
+ text.set_color(color)
619
+ else:
620
+ raise ValueError(f"Invalid labelcolor: {labelcolor!r}")
621
+
622
+ def _set_artist_props(self, a):
623
+ """
624
+ Set the boilerplate props for artists added to Axes.
625
+ """
626
+ a.set_figure(self.get_figure(root=False))
627
+ if self.isaxes:
628
+ a.axes = self.axes
629
+
630
+ a.set_transform(self.get_transform())
631
+
632
+ @_docstring.interpd
633
+ def set_loc(self, loc=None):
634
+ """
635
+ Set the location of the legend.
636
+
637
+ .. versionadded:: 3.8
638
+
639
+ Parameters
640
+ ----------
641
+ %(_legend_kw_set_loc_doc)s
642
+ """
643
+ loc0 = loc
644
+ self._loc_used_default = loc is None
645
+ if loc is None:
646
+ loc = mpl.rcParams["legend.loc"]
647
+ if not self.isaxes and loc in [0, 'best']:
648
+ loc = 'upper right'
649
+
650
+ type_err_message = ("loc must be string, coordinate tuple, or"
651
+ f" an integer 0-10, not {loc!r}")
652
+
653
+ # handle outside legends:
654
+ self._outside_loc = None
655
+ if isinstance(loc, str):
656
+ if loc.split()[0] == 'outside':
657
+ # strip outside:
658
+ loc = loc.split('outside ')[1]
659
+ # strip "center" at the beginning
660
+ self._outside_loc = loc.replace('center ', '')
661
+ # strip first
662
+ self._outside_loc = self._outside_loc.split()[0]
663
+ locs = loc.split()
664
+ if len(locs) > 1 and locs[0] in ('right', 'left'):
665
+ # locs doesn't accept "left upper", etc, so swap
666
+ if locs[0] != 'center':
667
+ locs = locs[::-1]
668
+ loc = locs[0] + ' ' + locs[1]
669
+ # check that loc is in acceptable strings
670
+ loc = _api.check_getitem(self.codes, loc=loc)
671
+ elif np.iterable(loc):
672
+ # coerce iterable into tuple
673
+ loc = tuple(loc)
674
+ # validate the tuple represents Real coordinates
675
+ if len(loc) != 2 or not all(isinstance(e, numbers.Real) for e in loc):
676
+ raise ValueError(type_err_message)
677
+ elif isinstance(loc, int):
678
+ # validate the integer represents a string numeric value
679
+ if loc < 0 or loc > 10:
680
+ raise ValueError(type_err_message)
681
+ else:
682
+ # all other cases are invalid values of loc
683
+ raise ValueError(type_err_message)
684
+
685
+ if self.isaxes and self._outside_loc:
686
+ raise ValueError(
687
+ f"'outside' option for loc='{loc0}' keyword argument only "
688
+ "works for figure legends")
689
+
690
+ if not self.isaxes and loc == 0:
691
+ raise ValueError(
692
+ "Automatic legend placement (loc='best') not implemented for "
693
+ "figure legend")
694
+
695
+ tmp = self._loc_used_default
696
+ self._set_loc(loc)
697
+ self._loc_used_default = tmp # ignore changes done by _set_loc
698
+
699
+ def _set_loc(self, loc):
700
+ # find_offset function will be provided to _legend_box and
701
+ # _legend_box will draw itself at the location of the return
702
+ # value of the find_offset.
703
+ self._loc_used_default = False
704
+ self._loc_real = loc
705
+ self.stale = True
706
+ self._legend_box.set_offset(self._findoffset)
707
+
708
+ def set_ncols(self, ncols):
709
+ """Set the number of columns."""
710
+ self._ncols = ncols
711
+
712
+ def _get_loc(self):
713
+ return self._loc_real
714
+
715
+ _loc = property(_get_loc, _set_loc)
716
+
717
+ def _findoffset(self, width, height, xdescent, ydescent, renderer):
718
+ """Helper function to locate the legend."""
719
+
720
+ if self._loc == 0: # "best".
721
+ x, y = self._find_best_position(width, height, renderer)
722
+ elif self._loc in Legend.codes.values(): # Fixed location.
723
+ bbox = Bbox.from_bounds(0, 0, width, height)
724
+ x, y = self._get_anchored_bbox(self._loc, bbox,
725
+ self.get_bbox_to_anchor(),
726
+ renderer)
727
+ else: # Axes or figure coordinates.
728
+ fx, fy = self._loc
729
+ bbox = self.get_bbox_to_anchor()
730
+ x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
731
+
732
+ return x + xdescent, y + ydescent
733
+
734
+ @allow_rasterization
735
+ def draw(self, renderer):
736
+ # docstring inherited
737
+ if not self.get_visible():
738
+ return
739
+
740
+ renderer.open_group('legend', gid=self.get_gid())
741
+
742
+ fontsize = renderer.points_to_pixels(self._fontsize)
743
+
744
+ # if mode == fill, set the width of the legend_box to the
745
+ # width of the parent (minus pads)
746
+ if self._mode in ["expand"]:
747
+ pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
748
+ self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
749
+
750
+ # update the location and size of the legend. This needs to
751
+ # be done in any case to clip the figure right.
752
+ bbox = self._legend_box.get_window_extent(renderer)
753
+ self.legendPatch.set_bounds(bbox.bounds)
754
+ self.legendPatch.set_mutation_scale(fontsize)
755
+
756
+ # self.shadow is validated in __init__
757
+ # So by here it is a bool and self._shadow_props contains any configs
758
+
759
+ if self.shadow:
760
+ Shadow(self.legendPatch, **self._shadow_props).draw(renderer)
761
+
762
+ self.legendPatch.draw(renderer)
763
+ self._legend_box.draw(renderer)
764
+
765
+ renderer.close_group('legend')
766
+ self.stale = False
767
+
768
+ # _default_handler_map defines the default mapping between plot
769
+ # elements and the legend handlers.
770
+
771
+ _default_handler_map = {
772
+ StemContainer: legend_handler.HandlerStem(),
773
+ ErrorbarContainer: legend_handler.HandlerErrorbar(),
774
+ Line2D: legend_handler.HandlerLine2D(),
775
+ Patch: legend_handler.HandlerPatch(),
776
+ StepPatch: legend_handler.HandlerStepPatch(),
777
+ LineCollection: legend_handler.HandlerLineCollection(),
778
+ RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
779
+ CircleCollection: legend_handler.HandlerCircleCollection(),
780
+ BarContainer: legend_handler.HandlerPatch(
781
+ update_func=legend_handler.update_from_first_child),
782
+ tuple: legend_handler.HandlerTuple(),
783
+ PathCollection: legend_handler.HandlerPathCollection(),
784
+ PolyCollection: legend_handler.HandlerPolyCollection()
785
+ }
786
+
787
+ # (get|set|update)_default_handler_maps are public interfaces to
788
+ # modify the default handler map.
789
+
790
+ @classmethod
791
+ def get_default_handler_map(cls):
792
+ """Return the global default handler map, shared by all legends."""
793
+ return cls._default_handler_map
794
+
795
+ @classmethod
796
+ def set_default_handler_map(cls, handler_map):
797
+ """Set the global default handler map, shared by all legends."""
798
+ cls._default_handler_map = handler_map
799
+
800
+ @classmethod
801
+ def update_default_handler_map(cls, handler_map):
802
+ """Update the global default handler map, shared by all legends."""
803
+ cls._default_handler_map.update(handler_map)
804
+
805
+ def get_legend_handler_map(self):
806
+ """Return this legend instance's handler map."""
807
+ default_handler_map = self.get_default_handler_map()
808
+ return ({**default_handler_map, **self._custom_handler_map}
809
+ if self._custom_handler_map else default_handler_map)
810
+
811
+ @staticmethod
812
+ def get_legend_handler(legend_handler_map, orig_handle):
813
+ """
814
+ Return a legend handler from *legend_handler_map* that
815
+ corresponds to *orig_handler*.
816
+
817
+ *legend_handler_map* should be a dictionary object (that is
818
+ returned by the get_legend_handler_map method).
819
+
820
+ It first checks if the *orig_handle* itself is a key in the
821
+ *legend_handler_map* and return the associated value.
822
+ Otherwise, it checks for each of the classes in its
823
+ method-resolution-order. If no matching key is found, it
824
+ returns ``None``.
825
+ """
826
+ try:
827
+ return legend_handler_map[orig_handle]
828
+ except (TypeError, KeyError): # TypeError if unhashable.
829
+ pass
830
+ for handle_type in type(orig_handle).mro():
831
+ try:
832
+ return legend_handler_map[handle_type]
833
+ except KeyError:
834
+ pass
835
+ return None
836
+
837
+ def _init_legend_box(self, handles, labels, markerfirst=True):
838
+ """
839
+ Initialize the legend_box. The legend_box is an instance of
840
+ the OffsetBox, which is packed with legend handles and
841
+ texts. Once packed, their location is calculated during the
842
+ drawing time.
843
+ """
844
+
845
+ fontsize = self._fontsize
846
+
847
+ # legend_box is a HPacker, horizontally packed with columns.
848
+ # Each column is a VPacker, vertically packed with legend items.
849
+ # Each legend item is a HPacker packed with:
850
+ # - handlebox: a DrawingArea which contains the legend handle.
851
+ # - labelbox: a TextArea which contains the legend text.
852
+
853
+ text_list = [] # the list of text instances
854
+ handle_list = [] # the list of handle instances
855
+ handles_and_labels = []
856
+
857
+ # The approximate height and descent of text. These values are
858
+ # only used for plotting the legend handle.
859
+ descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
860
+ height = fontsize * self.handleheight - descent
861
+ # each handle needs to be drawn inside a box of (x, y, w, h) =
862
+ # (0, -descent, width, height). And their coordinates should
863
+ # be given in the display coordinates.
864
+
865
+ # The transformation of each handle will be automatically set
866
+ # to self.get_transform(). If the artist does not use its
867
+ # default transform (e.g., Collections), you need to
868
+ # manually set their transform to the self.get_transform().
869
+ legend_handler_map = self.get_legend_handler_map()
870
+
871
+ for orig_handle, label in zip(handles, labels):
872
+ handler = self.get_legend_handler(legend_handler_map, orig_handle)
873
+ if handler is None:
874
+ _api.warn_external(
875
+ "Legend does not support handles for "
876
+ f"{type(orig_handle).__name__} "
877
+ "instances.\nA proxy artist may be used "
878
+ "instead.\nSee: https://matplotlib.org/"
879
+ "stable/users/explain/axes/legend_guide.html"
880
+ "#controlling-the-legend-entries")
881
+ # No handle for this artist, so we just defer to None.
882
+ handle_list.append(None)
883
+ else:
884
+ textbox = TextArea(label, multilinebaseline=True,
885
+ textprops=dict(
886
+ verticalalignment='baseline',
887
+ horizontalalignment='left',
888
+ fontproperties=self.prop))
889
+ handlebox = DrawingArea(width=self.handlelength * fontsize,
890
+ height=height,
891
+ xdescent=0., ydescent=descent)
892
+
893
+ text_list.append(textbox._text)
894
+ # Create the artist for the legend which represents the
895
+ # original artist/handle.
896
+ handle_list.append(handler.legend_artist(self, orig_handle,
897
+ fontsize, handlebox))
898
+ handles_and_labels.append((handlebox, textbox))
899
+
900
+ columnbox = []
901
+ # array_split splits n handles_and_labels into ncols columns, with the
902
+ # first n%ncols columns having an extra entry. filter(len, ...)
903
+ # handles the case where n < ncols: the last ncols-n columns are empty
904
+ # and get filtered out.
905
+ for handles_and_labels_column in filter(
906
+ len, np.array_split(handles_and_labels, self._ncols)):
907
+ # pack handlebox and labelbox into itembox
908
+ itemboxes = [HPacker(pad=0,
909
+ sep=self.handletextpad * fontsize,
910
+ children=[h, t] if markerfirst else [t, h],
911
+ align="baseline")
912
+ for h, t in handles_and_labels_column]
913
+ # pack columnbox
914
+ alignment = "baseline" if markerfirst else "right"
915
+ columnbox.append(VPacker(pad=0,
916
+ sep=self.labelspacing * fontsize,
917
+ align=alignment,
918
+ children=itemboxes))
919
+
920
+ mode = "expand" if self._mode == "expand" else "fixed"
921
+ sep = self.columnspacing * fontsize
922
+ self._legend_handle_box = HPacker(pad=0,
923
+ sep=sep, align="baseline",
924
+ mode=mode,
925
+ children=columnbox)
926
+ self._legend_title_box = TextArea("")
927
+ self._legend_box = VPacker(pad=self.borderpad * fontsize,
928
+ sep=self.labelspacing * fontsize,
929
+ align=self._alignment,
930
+ children=[self._legend_title_box,
931
+ self._legend_handle_box])
932
+ self._legend_box.set_figure(self.get_figure(root=False))
933
+ self._legend_box.axes = self.axes
934
+ self.texts = text_list
935
+ self.legend_handles = handle_list
936
+
937
+ def _auto_legend_data(self, renderer):
938
+ """
939
+ Return display coordinates for hit testing for "best" positioning.
940
+
941
+ Returns
942
+ -------
943
+ bboxes
944
+ List of bounding boxes of all patches.
945
+ lines
946
+ List of `.Path` corresponding to each line.
947
+ offsets
948
+ List of (x, y) offsets of all collection.
949
+ """
950
+ assert self.isaxes # always holds, as this is only called internally
951
+ bboxes = []
952
+ lines = []
953
+ offsets = []
954
+ for artist in self.parent._children:
955
+ if isinstance(artist, Line2D):
956
+ lines.append(
957
+ artist.get_transform().transform_path(artist.get_path()))
958
+ elif isinstance(artist, Rectangle):
959
+ bboxes.append(
960
+ artist.get_bbox().transformed(artist.get_data_transform()))
961
+ elif isinstance(artist, Patch):
962
+ lines.append(
963
+ artist.get_transform().transform_path(artist.get_path()))
964
+ elif isinstance(artist, PolyCollection):
965
+ lines.extend(artist.get_transform().transform_path(path)
966
+ for path in artist.get_paths())
967
+ elif isinstance(artist, Collection):
968
+ transform, transOffset, hoffsets, _ = artist._prepare_points()
969
+ if len(hoffsets):
970
+ offsets.extend(transOffset.transform(hoffsets))
971
+ elif isinstance(artist, Text):
972
+ bboxes.append(artist.get_window_extent(renderer))
973
+
974
+ return bboxes, lines, offsets
975
+
976
+ def get_children(self):
977
+ # docstring inherited
978
+ return [self._legend_box, self.get_frame()]
979
+
980
+ def get_frame(self):
981
+ """Return the `~.patches.Rectangle` used to frame the legend."""
982
+ return self.legendPatch
983
+
984
+ def get_lines(self):
985
+ r"""Return the list of `~.lines.Line2D`\s in the legend."""
986
+ return [h for h in self.legend_handles if isinstance(h, Line2D)]
987
+
988
+ def get_patches(self):
989
+ r"""Return the list of `~.patches.Patch`\s in the legend."""
990
+ return silent_list('Patch',
991
+ [h for h in self.legend_handles
992
+ if isinstance(h, Patch)])
993
+
994
+ def get_texts(self):
995
+ r"""Return the list of `~.text.Text`\s in the legend."""
996
+ return silent_list('Text', self.texts)
997
+
998
+ def set_alignment(self, alignment):
999
+ """
1000
+ Set the alignment of the legend title and the box of entries.
1001
+
1002
+ The entries are aligned as a single block, so that markers always
1003
+ lined up.
1004
+
1005
+ Parameters
1006
+ ----------
1007
+ alignment : {'center', 'left', 'right'}.
1008
+
1009
+ """
1010
+ _api.check_in_list(["center", "left", "right"], alignment=alignment)
1011
+ self._alignment = alignment
1012
+ self._legend_box.align = alignment
1013
+
1014
+ def get_alignment(self):
1015
+ """Get the alignment value of the legend box"""
1016
+ return self._legend_box.align
1017
+
1018
+ def set_title(self, title, prop=None):
1019
+ """
1020
+ Set legend title and title style.
1021
+
1022
+ Parameters
1023
+ ----------
1024
+ title : str
1025
+ The legend title.
1026
+
1027
+ prop : `.font_manager.FontProperties` or `str` or `pathlib.Path`
1028
+ The font properties of the legend title.
1029
+ If a `str`, it is interpreted as a fontconfig pattern parsed by
1030
+ `.FontProperties`. If a `pathlib.Path`, it is interpreted as the
1031
+ absolute path to a font file.
1032
+
1033
+ """
1034
+ self._legend_title_box._text.set_text(title)
1035
+ if title:
1036
+ self._legend_title_box._text.set_visible(True)
1037
+ self._legend_title_box.set_visible(True)
1038
+ else:
1039
+ self._legend_title_box._text.set_visible(False)
1040
+ self._legend_title_box.set_visible(False)
1041
+
1042
+ if prop is not None:
1043
+ self._legend_title_box._text.set_fontproperties(prop)
1044
+
1045
+ self.stale = True
1046
+
1047
+ def get_title(self):
1048
+ """Return the `.Text` instance for the legend title."""
1049
+ return self._legend_title_box._text
1050
+
1051
+ def get_window_extent(self, renderer=None):
1052
+ # docstring inherited
1053
+ if renderer is None:
1054
+ renderer = self.get_figure(root=True)._get_renderer()
1055
+ return self._legend_box.get_window_extent(renderer=renderer)
1056
+
1057
+ def get_tightbbox(self, renderer=None):
1058
+ # docstring inherited
1059
+ return self._legend_box.get_window_extent(renderer)
1060
+
1061
+ def get_frame_on(self):
1062
+ """Get whether the legend box patch is drawn."""
1063
+ return self.legendPatch.get_visible()
1064
+
1065
+ def set_frame_on(self, b):
1066
+ """
1067
+ Set whether the legend box patch is drawn.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ b : bool
1072
+ """
1073
+ self.legendPatch.set_visible(b)
1074
+ self.stale = True
1075
+
1076
+ draw_frame = set_frame_on # Backcompat alias.
1077
+
1078
+ def get_bbox_to_anchor(self):
1079
+ """Return the bbox that the legend will be anchored to."""
1080
+ if self._bbox_to_anchor is None:
1081
+ return self.parent.bbox
1082
+ else:
1083
+ return self._bbox_to_anchor
1084
+
1085
+ def set_bbox_to_anchor(self, bbox, transform=None):
1086
+ """
1087
+ Set the bbox that the legend will be anchored to.
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ bbox : `~matplotlib.transforms.BboxBase` or tuple
1092
+ The bounding box can be specified in the following ways:
1093
+
1094
+ - A `.BboxBase` instance
1095
+ - A tuple of ``(left, bottom, width, height)`` in the given
1096
+ transform (normalized axes coordinate if None)
1097
+ - A tuple of ``(left, bottom)`` where the width and height will be
1098
+ assumed to be zero.
1099
+ - *None*, to remove the bbox anchoring, and use the parent bbox.
1100
+
1101
+ transform : `~matplotlib.transforms.Transform`, optional
1102
+ A transform to apply to the bounding box. If not specified, this
1103
+ will use a transform to the bounding box of the parent.
1104
+ """
1105
+ if bbox is None:
1106
+ self._bbox_to_anchor = None
1107
+ return
1108
+ elif isinstance(bbox, BboxBase):
1109
+ self._bbox_to_anchor = bbox
1110
+ else:
1111
+ try:
1112
+ l = len(bbox)
1113
+ except TypeError as err:
1114
+ raise ValueError(f"Invalid bbox: {bbox}") from err
1115
+
1116
+ if l == 2:
1117
+ bbox = [bbox[0], bbox[1], 0, 0]
1118
+
1119
+ self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1120
+
1121
+ if transform is None:
1122
+ transform = BboxTransformTo(self.parent.bbox)
1123
+
1124
+ self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
1125
+ transform)
1126
+ self.stale = True
1127
+
1128
+ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
1129
+ """
1130
+ Place the *bbox* inside the *parentbbox* according to a given
1131
+ location code. Return the (x, y) coordinate of the bbox.
1132
+
1133
+ Parameters
1134
+ ----------
1135
+ loc : int
1136
+ A location code in range(1, 11). This corresponds to the possible
1137
+ values for ``self._loc``, excluding "best".
1138
+ bbox : `~matplotlib.transforms.Bbox`
1139
+ bbox to be placed, in display coordinates.
1140
+ parentbbox : `~matplotlib.transforms.Bbox`
1141
+ A parent box which will contain the bbox, in display coordinates.
1142
+ """
1143
+ return offsetbox._get_anchored_bbox(
1144
+ loc, bbox, parentbbox,
1145
+ self.borderaxespad * renderer.points_to_pixels(self._fontsize))
1146
+
1147
+ def _find_best_position(self, width, height, renderer):
1148
+ """Determine the best location to place the legend."""
1149
+ assert self.isaxes # always holds, as this is only called internally
1150
+
1151
+ start_time = time.perf_counter()
1152
+
1153
+ bboxes, lines, offsets = self._auto_legend_data(renderer)
1154
+
1155
+ bbox = Bbox.from_bounds(0, 0, width, height)
1156
+
1157
+ candidates = []
1158
+ for idx in range(1, len(self.codes)):
1159
+ l, b = self._get_anchored_bbox(idx, bbox,
1160
+ self.get_bbox_to_anchor(),
1161
+ renderer)
1162
+ legendBox = Bbox.from_bounds(l, b, width, height)
1163
+ # XXX TODO: If markers are present, it would be good to take them
1164
+ # into account when checking vertex overlaps in the next line.
1165
+ badness = (sum(legendBox.count_contains(line.vertices)
1166
+ for line in lines)
1167
+ + legendBox.count_contains(offsets)
1168
+ + legendBox.count_overlaps(bboxes)
1169
+ + sum(line.intersects_bbox(legendBox, filled=False)
1170
+ for line in lines))
1171
+ # Include the index to favor lower codes in case of a tie.
1172
+ candidates.append((badness, idx, (l, b)))
1173
+ if badness == 0:
1174
+ break
1175
+
1176
+ _, _, (l, b) = min(candidates)
1177
+
1178
+ if self._loc_used_default and time.perf_counter() - start_time > 1:
1179
+ _api.warn_external(
1180
+ 'Creating legend with loc="best" can be slow with large '
1181
+ 'amounts of data.')
1182
+
1183
+ return l, b
1184
+
1185
+ def contains(self, mouseevent):
1186
+ return self.legendPatch.contains(mouseevent)
1187
+
1188
+ def set_draggable(self, state, use_blit=False, update='loc'):
1189
+ """
1190
+ Enable or disable mouse dragging support of the legend.
1191
+
1192
+ Parameters
1193
+ ----------
1194
+ state : bool
1195
+ Whether mouse dragging is enabled.
1196
+ use_blit : bool, optional
1197
+ Use blitting for faster image composition. For details see
1198
+ :ref:`func-animation`.
1199
+ update : {'loc', 'bbox'}, optional
1200
+ The legend parameter to be changed when dragged:
1201
+
1202
+ - 'loc': update the *loc* parameter of the legend
1203
+ - 'bbox': update the *bbox_to_anchor* parameter of the legend
1204
+
1205
+ Returns
1206
+ -------
1207
+ `.DraggableLegend` or *None*
1208
+ If *state* is ``True`` this returns the `.DraggableLegend` helper
1209
+ instance. Otherwise this returns *None*.
1210
+ """
1211
+ if state:
1212
+ if self._draggable is None:
1213
+ self._draggable = DraggableLegend(self,
1214
+ use_blit,
1215
+ update=update)
1216
+ else:
1217
+ if self._draggable is not None:
1218
+ self._draggable.disconnect()
1219
+ self._draggable = None
1220
+ return self._draggable
1221
+
1222
+ def get_draggable(self):
1223
+ """Return ``True`` if the legend is draggable, ``False`` otherwise."""
1224
+ return self._draggable is not None
1225
+
1226
+
1227
+ # Helper functions to parse legend arguments for both `figure.legend` and
1228
+ # `axes.legend`:
1229
+ def _get_legend_handles(axs, legend_handler_map=None):
1230
+ """Yield artists that can be used as handles in a legend."""
1231
+ handles_original = []
1232
+ for ax in axs:
1233
+ handles_original += [
1234
+ *(a for a in ax._children
1235
+ if isinstance(a, (Line2D, Patch, Collection, Text))),
1236
+ *ax.containers]
1237
+ # support parasite Axes:
1238
+ if hasattr(ax, 'parasites'):
1239
+ for axx in ax.parasites:
1240
+ handles_original += [
1241
+ *(a for a in axx._children
1242
+ if isinstance(a, (Line2D, Patch, Collection, Text))),
1243
+ *axx.containers]
1244
+
1245
+ handler_map = {**Legend.get_default_handler_map(),
1246
+ **(legend_handler_map or {})}
1247
+ has_handler = Legend.get_legend_handler
1248
+ for handle in handles_original:
1249
+ label = handle.get_label()
1250
+ if label != '_nolegend_' and has_handler(handler_map, handle):
1251
+ yield handle
1252
+ elif (label and not label.startswith('_') and
1253
+ not has_handler(handler_map, handle)):
1254
+ _api.warn_external(
1255
+ "Legend does not support handles for "
1256
+ f"{type(handle).__name__} "
1257
+ "instances.\nSee: https://matplotlib.org/stable/"
1258
+ "tutorials/intermediate/legend_guide.html"
1259
+ "#implementing-a-custom-legend-handler")
1260
+ continue
1261
+
1262
+
1263
+ def _get_legend_handles_labels(axs, legend_handler_map=None):
1264
+ """Return handles and labels for legend."""
1265
+ handles = []
1266
+ labels = []
1267
+ for handle in _get_legend_handles(axs, legend_handler_map):
1268
+ label = handle.get_label()
1269
+ if label and not label.startswith('_'):
1270
+ handles.append(handle)
1271
+ labels.append(label)
1272
+ return handles, labels
1273
+
1274
+
1275
+ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
1276
+ """
1277
+ Get the handles and labels from the calls to either ``figure.legend``
1278
+ or ``axes.legend``.
1279
+
1280
+ The parser is a bit involved because we support::
1281
+
1282
+ legend()
1283
+ legend(labels)
1284
+ legend(handles, labels)
1285
+ legend(labels=labels)
1286
+ legend(handles=handles)
1287
+ legend(handles=handles, labels=labels)
1288
+
1289
+ The behavior for a mixture of positional and keyword handles and labels
1290
+ is undefined and issues a warning; it will be an error in the future.
1291
+
1292
+ Parameters
1293
+ ----------
1294
+ axs : list of `.Axes`
1295
+ If handles are not given explicitly, the artists in these Axes are
1296
+ used as handles.
1297
+ *args : tuple
1298
+ Positional parameters passed to ``legend()``.
1299
+ handles
1300
+ The value of the keyword argument ``legend(handles=...)``, or *None*
1301
+ if that keyword argument was not used.
1302
+ labels
1303
+ The value of the keyword argument ``legend(labels=...)``, or *None*
1304
+ if that keyword argument was not used.
1305
+ **kwargs
1306
+ All other keyword arguments passed to ``legend()``.
1307
+
1308
+ Returns
1309
+ -------
1310
+ handles : list of (`.Artist` or tuple of `.Artist`)
1311
+ The legend handles.
1312
+ labels : list of str
1313
+ The legend labels.
1314
+ kwargs : dict
1315
+ *kwargs* with keywords handles and labels removed.
1316
+
1317
+ """
1318
+ log = logging.getLogger(__name__)
1319
+
1320
+ handlers = kwargs.get('handler_map')
1321
+
1322
+ if (handles is not None or labels is not None) and args:
1323
+ _api.warn_deprecated("3.9", message=(
1324
+ "You have mixed positional and keyword arguments, some input may "
1325
+ "be discarded. This is deprecated since %(since)s and will "
1326
+ "become an error in %(removal)s."))
1327
+
1328
+ if (hasattr(handles, "__len__") and
1329
+ hasattr(labels, "__len__") and
1330
+ len(handles) != len(labels)):
1331
+ _api.warn_external(f"Mismatched number of handles and labels: "
1332
+ f"len(handles) = {len(handles)} "
1333
+ f"len(labels) = {len(labels)}")
1334
+ # if got both handles and labels as kwargs, make same length
1335
+ if handles and labels:
1336
+ handles, labels = zip(*zip(handles, labels))
1337
+
1338
+ elif handles is not None and labels is None:
1339
+ labels = [handle.get_label() for handle in handles]
1340
+
1341
+ elif labels is not None and handles is None:
1342
+ # Get as many handles as there are labels.
1343
+ handles = [handle for handle, label
1344
+ in zip(_get_legend_handles(axs, handlers), labels)]
1345
+
1346
+ elif len(args) == 0: # 0 args: automatically detect labels and handles.
1347
+ handles, labels = _get_legend_handles_labels(axs, handlers)
1348
+ if not handles:
1349
+ _api.warn_external(
1350
+ "No artists with labels found to put in legend. Note that "
1351
+ "artists whose label start with an underscore are ignored "
1352
+ "when legend() is called with no argument.")
1353
+
1354
+ elif len(args) == 1: # 1 arg: user defined labels, automatic handle detection.
1355
+ labels, = args
1356
+ if any(isinstance(l, Artist) for l in labels):
1357
+ raise TypeError("A single argument passed to legend() must be a "
1358
+ "list of labels, but found an Artist in there.")
1359
+
1360
+ # Get as many handles as there are labels.
1361
+ handles = [handle for handle, label
1362
+ in zip(_get_legend_handles(axs, handlers), labels)]
1363
+
1364
+ elif len(args) == 2: # 2 args: user defined handles and labels.
1365
+ handles, labels = args[:2]
1366
+
1367
+ else:
1368
+ raise _api.nargs_error('legend', '0-2', len(args))
1369
+
1370
+ return handles, labels, kwargs
lib/python3.12/site-packages/matplotlib/lines.py ADDED
@@ -0,0 +1,1720 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 2D lines with support for a variety of line styles, markers, colors, etc.
3
+ """
4
+
5
+ import copy
6
+
7
+ from numbers import Integral, Number, Real
8
+ import logging
9
+
10
+ import numpy as np
11
+
12
+ import matplotlib as mpl
13
+ from . import _api, cbook, colors as mcolors, _docstring
14
+ from .artist import Artist, allow_rasterization
15
+ from .cbook import (
16
+ _to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP)
17
+ from .markers import MarkerStyle
18
+ from .path import Path
19
+ from .transforms import Bbox, BboxTransformTo, TransformedPath
20
+ from ._enums import JoinStyle, CapStyle
21
+
22
+ # Imported here for backward compatibility, even though they don't
23
+ # really belong.
24
+ from . import _path
25
+ from .markers import ( # noqa
26
+ CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN,
27
+ CARETLEFTBASE, CARETRIGHTBASE, CARETUPBASE, CARETDOWNBASE,
28
+ TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN)
29
+
30
+ _log = logging.getLogger(__name__)
31
+
32
+
33
+ def _get_dash_pattern(style):
34
+ """Convert linestyle to dash pattern."""
35
+ # go from short hand -> full strings
36
+ if isinstance(style, str):
37
+ style = ls_mapper.get(style, style)
38
+ # un-dashed styles
39
+ if style in ['solid', 'None']:
40
+ offset = 0
41
+ dashes = None
42
+ # dashed styles
43
+ elif style in ['dashed', 'dashdot', 'dotted']:
44
+ offset = 0
45
+ dashes = tuple(mpl.rcParams[f'lines.{style}_pattern'])
46
+ #
47
+ elif isinstance(style, tuple):
48
+ offset, dashes = style
49
+ if offset is None:
50
+ raise ValueError(f'Unrecognized linestyle: {style!r}')
51
+ else:
52
+ raise ValueError(f'Unrecognized linestyle: {style!r}')
53
+
54
+ # normalize offset to be positive and shorter than the dash cycle
55
+ if dashes is not None:
56
+ dsum = sum(dashes)
57
+ if dsum:
58
+ offset %= dsum
59
+
60
+ return offset, dashes
61
+
62
+
63
+ def _get_dash_patterns(styles):
64
+ """Convert linestyle or sequence of linestyles to list of dash patterns."""
65
+ try:
66
+ patterns = [_get_dash_pattern(styles)]
67
+ except ValueError:
68
+ try:
69
+ patterns = [_get_dash_pattern(x) for x in styles]
70
+ except ValueError as err:
71
+ emsg = f'Do not know how to convert {styles!r} to dashes'
72
+ raise ValueError(emsg) from err
73
+
74
+ return patterns
75
+
76
+
77
+ def _get_inverse_dash_pattern(offset, dashes):
78
+ """Return the inverse of the given dash pattern, for filling the gaps."""
79
+ # Define the inverse pattern by moving the last gap to the start of the
80
+ # sequence.
81
+ gaps = dashes[-1:] + dashes[:-1]
82
+ # Set the offset so that this new first segment is skipped
83
+ # (see backend_bases.GraphicsContextBase.set_dashes for offset definition).
84
+ offset_gaps = offset + dashes[-1]
85
+
86
+ return offset_gaps, gaps
87
+
88
+
89
+ def _scale_dashes(offset, dashes, lw):
90
+ if not mpl.rcParams['lines.scale_dashes']:
91
+ return offset, dashes
92
+ scaled_offset = offset * lw
93
+ scaled_dashes = ([x * lw if x is not None else None for x in dashes]
94
+ if dashes is not None else None)
95
+ return scaled_offset, scaled_dashes
96
+
97
+
98
+ def segment_hits(cx, cy, x, y, radius):
99
+ """
100
+ Return the indices of the segments in the polyline with coordinates (*cx*,
101
+ *cy*) that are within a distance *radius* of the point (*x*, *y*).
102
+ """
103
+ # Process single points specially
104
+ if len(x) <= 1:
105
+ res, = np.nonzero((cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2)
106
+ return res
107
+
108
+ # We need to lop the last element off a lot.
109
+ xr, yr = x[:-1], y[:-1]
110
+
111
+ # Only look at line segments whose nearest point to C on the line
112
+ # lies within the segment.
113
+ dx, dy = x[1:] - xr, y[1:] - yr
114
+ Lnorm_sq = dx ** 2 + dy ** 2 # Possibly want to eliminate Lnorm==0
115
+ u = ((cx - xr) * dx + (cy - yr) * dy) / Lnorm_sq
116
+ candidates = (u >= 0) & (u <= 1)
117
+
118
+ # Note that there is a little area near one side of each point
119
+ # which will be near neither segment, and another which will
120
+ # be near both, depending on the angle of the lines. The
121
+ # following radius test eliminates these ambiguities.
122
+ point_hits = (cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2
123
+ candidates = candidates & ~(point_hits[:-1] | point_hits[1:])
124
+
125
+ # For those candidates which remain, determine how far they lie away
126
+ # from the line.
127
+ px, py = xr + u * dx, yr + u * dy
128
+ line_hits = (cx - px) ** 2 + (cy - py) ** 2 <= radius ** 2
129
+ line_hits = line_hits & candidates
130
+ points, = point_hits.ravel().nonzero()
131
+ lines, = line_hits.ravel().nonzero()
132
+ return np.concatenate((points, lines))
133
+
134
+
135
+ def _mark_every_path(markevery, tpath, affine, ax):
136
+ """
137
+ Helper function that sorts out how to deal the input
138
+ `markevery` and returns the points where markers should be drawn.
139
+
140
+ Takes in the `markevery` value and the line path and returns the
141
+ sub-sampled path.
142
+ """
143
+ # pull out the two bits of data we want from the path
144
+ codes, verts = tpath.codes, tpath.vertices
145
+
146
+ def _slice_or_none(in_v, slc):
147
+ """Helper function to cope with `codes` being an ndarray or `None`."""
148
+ if in_v is None:
149
+ return None
150
+ return in_v[slc]
151
+
152
+ # if just an int, assume starting at 0 and make a tuple
153
+ if isinstance(markevery, Integral):
154
+ markevery = (0, markevery)
155
+ # if just a float, assume starting at 0.0 and make a tuple
156
+ elif isinstance(markevery, Real):
157
+ markevery = (0.0, markevery)
158
+
159
+ if isinstance(markevery, tuple):
160
+ if len(markevery) != 2:
161
+ raise ValueError('`markevery` is a tuple but its len is not 2; '
162
+ f'markevery={markevery}')
163
+ start, step = markevery
164
+ # if step is an int, old behavior
165
+ if isinstance(step, Integral):
166
+ # tuple of 2 int is for backwards compatibility,
167
+ if not isinstance(start, Integral):
168
+ raise ValueError(
169
+ '`markevery` is a tuple with len 2 and second element is '
170
+ 'an int, but the first element is not an int; '
171
+ f'markevery={markevery}')
172
+ # just return, we are done here
173
+
174
+ return Path(verts[slice(start, None, step)],
175
+ _slice_or_none(codes, slice(start, None, step)))
176
+
177
+ elif isinstance(step, Real):
178
+ if not isinstance(start, Real):
179
+ raise ValueError(
180
+ '`markevery` is a tuple with len 2 and second element is '
181
+ 'a float, but the first element is not a float or an int; '
182
+ f'markevery={markevery}')
183
+ if ax is None:
184
+ raise ValueError(
185
+ "markevery is specified relative to the Axes size, but "
186
+ "the line does not have a Axes as parent")
187
+
188
+ # calc cumulative distance along path (in display coords):
189
+ fin = np.isfinite(verts).all(axis=1)
190
+ fverts = verts[fin]
191
+ disp_coords = affine.transform(fverts)
192
+
193
+ delta = np.empty((len(disp_coords), 2))
194
+ delta[0, :] = 0
195
+ delta[1:, :] = disp_coords[1:, :] - disp_coords[:-1, :]
196
+ delta = np.hypot(*delta.T).cumsum()
197
+ # calc distance between markers along path based on the Axes
198
+ # bounding box diagonal being a distance of unity:
199
+ (x0, y0), (x1, y1) = ax.transAxes.transform([[0, 0], [1, 1]])
200
+ scale = np.hypot(x1 - x0, y1 - y0)
201
+ marker_delta = np.arange(start * scale, delta[-1], step * scale)
202
+ # find closest actual data point that is closest to
203
+ # the theoretical distance along the path:
204
+ inds = np.abs(delta[np.newaxis, :] - marker_delta[:, np.newaxis])
205
+ inds = inds.argmin(axis=1)
206
+ inds = np.unique(inds)
207
+ # return, we are done here
208
+ return Path(fverts[inds], _slice_or_none(codes, inds))
209
+ else:
210
+ raise ValueError(
211
+ f"markevery={markevery!r} is a tuple with len 2, but its "
212
+ f"second element is not an int or a float")
213
+
214
+ elif isinstance(markevery, slice):
215
+ # mazol tov, it's already a slice, just return
216
+ return Path(verts[markevery], _slice_or_none(codes, markevery))
217
+
218
+ elif np.iterable(markevery):
219
+ # fancy indexing
220
+ try:
221
+ return Path(verts[markevery], _slice_or_none(codes, markevery))
222
+ except (ValueError, IndexError) as err:
223
+ raise ValueError(
224
+ f"markevery={markevery!r} is iterable but not a valid numpy "
225
+ f"fancy index") from err
226
+ else:
227
+ raise ValueError(f"markevery={markevery!r} is not a recognized value")
228
+
229
+
230
+ @_docstring.interpd
231
+ @_api.define_aliases({
232
+ "antialiased": ["aa"],
233
+ "color": ["c"],
234
+ "drawstyle": ["ds"],
235
+ "linestyle": ["ls"],
236
+ "linewidth": ["lw"],
237
+ "markeredgecolor": ["mec"],
238
+ "markeredgewidth": ["mew"],
239
+ "markerfacecolor": ["mfc"],
240
+ "markerfacecoloralt": ["mfcalt"],
241
+ "markersize": ["ms"],
242
+ })
243
+ class Line2D(Artist):
244
+ """
245
+ A line - the line can have both a solid linestyle connecting all
246
+ the vertices, and a marker at each vertex. Additionally, the
247
+ drawing of the solid line is influenced by the drawstyle, e.g., one
248
+ can create "stepped" lines in various styles.
249
+ """
250
+
251
+ lineStyles = _lineStyles = { # hidden names deprecated
252
+ '-': '_draw_solid',
253
+ '--': '_draw_dashed',
254
+ '-.': '_draw_dash_dot',
255
+ ':': '_draw_dotted',
256
+ 'None': '_draw_nothing',
257
+ ' ': '_draw_nothing',
258
+ '': '_draw_nothing',
259
+ }
260
+
261
+ _drawStyles_l = {
262
+ 'default': '_draw_lines',
263
+ 'steps-mid': '_draw_steps_mid',
264
+ 'steps-pre': '_draw_steps_pre',
265
+ 'steps-post': '_draw_steps_post',
266
+ }
267
+
268
+ _drawStyles_s = {
269
+ 'steps': '_draw_steps_pre',
270
+ }
271
+
272
+ # drawStyles should now be deprecated.
273
+ drawStyles = {**_drawStyles_l, **_drawStyles_s}
274
+ # Need a list ordered with long names first:
275
+ drawStyleKeys = [*_drawStyles_l, *_drawStyles_s]
276
+
277
+ # Referenced here to maintain API. These are defined in
278
+ # MarkerStyle
279
+ markers = MarkerStyle.markers
280
+ filled_markers = MarkerStyle.filled_markers
281
+ fillStyles = MarkerStyle.fillstyles
282
+
283
+ zorder = 2
284
+
285
+ _subslice_optim_min_size = 1000
286
+
287
+ def __str__(self):
288
+ if self._label != "":
289
+ return f"Line2D({self._label})"
290
+ elif self._x is None:
291
+ return "Line2D()"
292
+ elif len(self._x) > 3:
293
+ return "Line2D(({:g},{:g}),({:g},{:g}),...,({:g},{:g}))".format(
294
+ self._x[0], self._y[0],
295
+ self._x[1], self._y[1],
296
+ self._x[-1], self._y[-1])
297
+ else:
298
+ return "Line2D(%s)" % ",".join(
299
+ map("({:g},{:g})".format, self._x, self._y))
300
+
301
+ def __init__(self, xdata, ydata, *,
302
+ linewidth=None, # all Nones default to rc
303
+ linestyle=None,
304
+ color=None,
305
+ gapcolor=None,
306
+ marker=None,
307
+ markersize=None,
308
+ markeredgewidth=None,
309
+ markeredgecolor=None,
310
+ markerfacecolor=None,
311
+ markerfacecoloralt='none',
312
+ fillstyle=None,
313
+ antialiased=None,
314
+ dash_capstyle=None,
315
+ solid_capstyle=None,
316
+ dash_joinstyle=None,
317
+ solid_joinstyle=None,
318
+ pickradius=5,
319
+ drawstyle=None,
320
+ markevery=None,
321
+ **kwargs
322
+ ):
323
+ """
324
+ Create a `.Line2D` instance with *x* and *y* data in sequences of
325
+ *xdata*, *ydata*.
326
+
327
+ Additional keyword arguments are `.Line2D` properties:
328
+
329
+ %(Line2D:kwdoc)s
330
+
331
+ See :meth:`set_linestyle` for a description of the line styles,
332
+ :meth:`set_marker` for a description of the markers, and
333
+ :meth:`set_drawstyle` for a description of the draw styles.
334
+
335
+ """
336
+ super().__init__()
337
+
338
+ # Convert sequences to NumPy arrays.
339
+ if not np.iterable(xdata):
340
+ raise RuntimeError('xdata must be a sequence')
341
+ if not np.iterable(ydata):
342
+ raise RuntimeError('ydata must be a sequence')
343
+
344
+ if linewidth is None:
345
+ linewidth = mpl.rcParams['lines.linewidth']
346
+
347
+ if linestyle is None:
348
+ linestyle = mpl.rcParams['lines.linestyle']
349
+ if marker is None:
350
+ marker = mpl.rcParams['lines.marker']
351
+ if color is None:
352
+ color = mpl.rcParams['lines.color']
353
+
354
+ if markersize is None:
355
+ markersize = mpl.rcParams['lines.markersize']
356
+ if antialiased is None:
357
+ antialiased = mpl.rcParams['lines.antialiased']
358
+ if dash_capstyle is None:
359
+ dash_capstyle = mpl.rcParams['lines.dash_capstyle']
360
+ if dash_joinstyle is None:
361
+ dash_joinstyle = mpl.rcParams['lines.dash_joinstyle']
362
+ if solid_capstyle is None:
363
+ solid_capstyle = mpl.rcParams['lines.solid_capstyle']
364
+ if solid_joinstyle is None:
365
+ solid_joinstyle = mpl.rcParams['lines.solid_joinstyle']
366
+
367
+ if drawstyle is None:
368
+ drawstyle = 'default'
369
+
370
+ self._dashcapstyle = None
371
+ self._dashjoinstyle = None
372
+ self._solidjoinstyle = None
373
+ self._solidcapstyle = None
374
+ self.set_dash_capstyle(dash_capstyle)
375
+ self.set_dash_joinstyle(dash_joinstyle)
376
+ self.set_solid_capstyle(solid_capstyle)
377
+ self.set_solid_joinstyle(solid_joinstyle)
378
+
379
+ self._linestyles = None
380
+ self._drawstyle = None
381
+ self._linewidth = linewidth
382
+ self._unscaled_dash_pattern = (0, None) # offset, dash
383
+ self._dash_pattern = (0, None) # offset, dash (scaled by linewidth)
384
+
385
+ self.set_linewidth(linewidth)
386
+ self.set_linestyle(linestyle)
387
+ self.set_drawstyle(drawstyle)
388
+
389
+ self._color = None
390
+ self.set_color(color)
391
+ if marker is None:
392
+ marker = 'none' # Default.
393
+ if not isinstance(marker, MarkerStyle):
394
+ self._marker = MarkerStyle(marker, fillstyle)
395
+ else:
396
+ self._marker = marker
397
+
398
+ self._gapcolor = None
399
+ self.set_gapcolor(gapcolor)
400
+
401
+ self._markevery = None
402
+ self._markersize = None
403
+ self._antialiased = None
404
+
405
+ self.set_markevery(markevery)
406
+ self.set_antialiased(antialiased)
407
+ self.set_markersize(markersize)
408
+
409
+ self._markeredgecolor = None
410
+ self._markeredgewidth = None
411
+ self._markerfacecolor = None
412
+ self._markerfacecoloralt = None
413
+
414
+ self.set_markerfacecolor(markerfacecolor) # Normalizes None to rc.
415
+ self.set_markerfacecoloralt(markerfacecoloralt)
416
+ self.set_markeredgecolor(markeredgecolor) # Normalizes None to rc.
417
+ self.set_markeredgewidth(markeredgewidth)
418
+
419
+ # update kwargs before updating data to give the caller a
420
+ # chance to init axes (and hence unit support)
421
+ self._internal_update(kwargs)
422
+ self.pickradius = pickradius
423
+ self.ind_offset = 0
424
+ if (isinstance(self._picker, Number) and
425
+ not isinstance(self._picker, bool)):
426
+ self._pickradius = self._picker
427
+
428
+ self._xorig = np.asarray([])
429
+ self._yorig = np.asarray([])
430
+ self._invalidx = True
431
+ self._invalidy = True
432
+ self._x = None
433
+ self._y = None
434
+ self._xy = None
435
+ self._path = None
436
+ self._transformed_path = None
437
+ self._subslice = False
438
+ self._x_filled = None # used in subslicing; only x is needed
439
+
440
+ self.set_data(xdata, ydata)
441
+
442
+ def contains(self, mouseevent):
443
+ """
444
+ Test whether *mouseevent* occurred on the line.
445
+
446
+ An event is deemed to have occurred "on" the line if it is less
447
+ than ``self.pickradius`` (default: 5 points) away from it. Use
448
+ `~.Line2D.get_pickradius` or `~.Line2D.set_pickradius` to get or set
449
+ the pick radius.
450
+
451
+ Parameters
452
+ ----------
453
+ mouseevent : `~matplotlib.backend_bases.MouseEvent`
454
+
455
+ Returns
456
+ -------
457
+ contains : bool
458
+ Whether any values are within the radius.
459
+ details : dict
460
+ A dictionary ``{'ind': pointlist}``, where *pointlist* is a
461
+ list of points of the line that are within the pickradius around
462
+ the event position.
463
+
464
+ TODO: sort returned indices by distance
465
+ """
466
+ if self._different_canvas(mouseevent):
467
+ return False, {}
468
+
469
+ # Make sure we have data to plot
470
+ if self._invalidy or self._invalidx:
471
+ self.recache()
472
+ if len(self._xy) == 0:
473
+ return False, {}
474
+
475
+ # Convert points to pixels
476
+ transformed_path = self._get_transformed_path()
477
+ path, affine = transformed_path.get_transformed_path_and_affine()
478
+ path = affine.transform_path(path)
479
+ xy = path.vertices
480
+ xt = xy[:, 0]
481
+ yt = xy[:, 1]
482
+
483
+ # Convert pick radius from points to pixels
484
+ fig = self.get_figure(root=True)
485
+ if fig is None:
486
+ _log.warning('no figure set when check if mouse is on line')
487
+ pixels = self._pickradius
488
+ else:
489
+ pixels = fig.dpi / 72. * self._pickradius
490
+
491
+ # The math involved in checking for containment (here and inside of
492
+ # segment_hits) assumes that it is OK to overflow, so temporarily set
493
+ # the error flags accordingly.
494
+ with np.errstate(all='ignore'):
495
+ # Check for collision
496
+ if self._linestyle in ['None', None]:
497
+ # If no line, return the nearby point(s)
498
+ ind, = np.nonzero(
499
+ (xt - mouseevent.x) ** 2 + (yt - mouseevent.y) ** 2
500
+ <= pixels ** 2)
501
+ else:
502
+ # If line, return the nearby segment(s)
503
+ ind = segment_hits(mouseevent.x, mouseevent.y, xt, yt, pixels)
504
+ if self._drawstyle.startswith("steps"):
505
+ ind //= 2
506
+
507
+ ind += self.ind_offset
508
+
509
+ # Return the point(s) within radius
510
+ return len(ind) > 0, dict(ind=ind)
511
+
512
+ def get_pickradius(self):
513
+ """
514
+ Return the pick radius used for containment tests.
515
+
516
+ See `.contains` for more details.
517
+ """
518
+ return self._pickradius
519
+
520
+ def set_pickradius(self, pickradius):
521
+ """
522
+ Set the pick radius used for containment tests.
523
+
524
+ See `.contains` for more details.
525
+
526
+ Parameters
527
+ ----------
528
+ pickradius : float
529
+ Pick radius, in points.
530
+ """
531
+ if not isinstance(pickradius, Real) or pickradius < 0:
532
+ raise ValueError("pick radius should be a distance")
533
+ self._pickradius = pickradius
534
+
535
+ pickradius = property(get_pickradius, set_pickradius)
536
+
537
+ def get_fillstyle(self):
538
+ """
539
+ Return the marker fill style.
540
+
541
+ See also `~.Line2D.set_fillstyle`.
542
+ """
543
+ return self._marker.get_fillstyle()
544
+
545
+ def set_fillstyle(self, fs):
546
+ """
547
+ Set the marker fill style.
548
+
549
+ Parameters
550
+ ----------
551
+ fs : {'full', 'left', 'right', 'bottom', 'top', 'none'}
552
+ Possible values:
553
+
554
+ - 'full': Fill the whole marker with the *markerfacecolor*.
555
+ - 'left', 'right', 'bottom', 'top': Fill the marker half at
556
+ the given side with the *markerfacecolor*. The other
557
+ half of the marker is filled with *markerfacecoloralt*.
558
+ - 'none': No filling.
559
+
560
+ For examples see :ref:`marker_fill_styles`.
561
+ """
562
+ self.set_marker(MarkerStyle(self._marker.get_marker(), fs))
563
+ self.stale = True
564
+
565
+ def set_markevery(self, every):
566
+ """
567
+ Set the markevery property to subsample the plot when using markers.
568
+
569
+ e.g., if ``every=5``, every 5-th marker will be plotted.
570
+
571
+ Parameters
572
+ ----------
573
+ every : None or int or (int, int) or slice or list[int] or float or \
574
+ (float, float) or list[bool]
575
+ Which markers to plot.
576
+
577
+ - ``every=None``: every point will be plotted.
578
+ - ``every=N``: every N-th marker will be plotted starting with
579
+ marker 0.
580
+ - ``every=(start, N)``: every N-th marker, starting at index
581
+ *start*, will be plotted.
582
+ - ``every=slice(start, end, N)``: every N-th marker, starting at
583
+ index *start*, up to but not including index *end*, will be
584
+ plotted.
585
+ - ``every=[i, j, m, ...]``: only markers at the given indices
586
+ will be plotted.
587
+ - ``every=[True, False, True, ...]``: only positions that are True
588
+ will be plotted. The list must have the same length as the data
589
+ points.
590
+ - ``every=0.1``, (i.e. a float): markers will be spaced at
591
+ approximately equal visual distances along the line; the distance
592
+ along the line between markers is determined by multiplying the
593
+ display-coordinate distance of the Axes bounding-box diagonal
594
+ by the value of *every*.
595
+ - ``every=(0.5, 0.1)`` (i.e. a length-2 tuple of float): similar
596
+ to ``every=0.1`` but the first marker will be offset along the
597
+ line by 0.5 multiplied by the
598
+ display-coordinate-diagonal-distance along the line.
599
+
600
+ For examples see
601
+ :doc:`/gallery/lines_bars_and_markers/markevery_demo`.
602
+
603
+ Notes
604
+ -----
605
+ Setting *markevery* will still only draw markers at actual data points.
606
+ While the float argument form aims for uniform visual spacing, it has
607
+ to coerce from the ideal spacing to the nearest available data point.
608
+ Depending on the number and distribution of data points, the result
609
+ may still not look evenly spaced.
610
+
611
+ When using a start offset to specify the first marker, the offset will
612
+ be from the first data point which may be different from the first
613
+ the visible data point if the plot is zoomed in.
614
+
615
+ If zooming in on a plot when using float arguments then the actual
616
+ data points that have markers will change because the distance between
617
+ markers is always determined from the display-coordinates
618
+ axes-bounding-box-diagonal regardless of the actual axes data limits.
619
+
620
+ """
621
+ self._markevery = every
622
+ self.stale = True
623
+
624
+ def get_markevery(self):
625
+ """
626
+ Return the markevery setting for marker subsampling.
627
+
628
+ See also `~.Line2D.set_markevery`.
629
+ """
630
+ return self._markevery
631
+
632
+ def set_picker(self, p):
633
+ """
634
+ Set the event picker details for the line.
635
+
636
+ Parameters
637
+ ----------
638
+ p : float or callable[[Artist, Event], tuple[bool, dict]]
639
+ If a float, it is used as the pick radius in points.
640
+ """
641
+ if not callable(p):
642
+ self.set_pickradius(p)
643
+ self._picker = p
644
+
645
+ def get_bbox(self):
646
+ """Get the bounding box of this line."""
647
+ bbox = Bbox([[0, 0], [0, 0]])
648
+ bbox.update_from_data_xy(self.get_xydata())
649
+ return bbox
650
+
651
+ def get_window_extent(self, renderer=None):
652
+ bbox = Bbox([[0, 0], [0, 0]])
653
+ trans_data_to_xy = self.get_transform().transform
654
+ bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()),
655
+ ignore=True)
656
+ # correct for marker size, if any
657
+ if self._marker:
658
+ ms = (self._markersize / 72.0 * self.get_figure(root=True).dpi) * 0.5
659
+ bbox = bbox.padded(ms)
660
+ return bbox
661
+
662
+ def set_data(self, *args):
663
+ """
664
+ Set the x and y data.
665
+
666
+ Parameters
667
+ ----------
668
+ *args : (2, N) array or two 1D arrays
669
+
670
+ See Also
671
+ --------
672
+ set_xdata
673
+ set_ydata
674
+ """
675
+ if len(args) == 1:
676
+ (x, y), = args
677
+ else:
678
+ x, y = args
679
+
680
+ self.set_xdata(x)
681
+ self.set_ydata(y)
682
+
683
+ def recache_always(self):
684
+ self.recache(always=True)
685
+
686
+ def recache(self, always=False):
687
+ if always or self._invalidx:
688
+ xconv = self.convert_xunits(self._xorig)
689
+ x = _to_unmasked_float_array(xconv).ravel()
690
+ else:
691
+ x = self._x
692
+ if always or self._invalidy:
693
+ yconv = self.convert_yunits(self._yorig)
694
+ y = _to_unmasked_float_array(yconv).ravel()
695
+ else:
696
+ y = self._y
697
+
698
+ self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float)
699
+ self._x, self._y = self._xy.T # views
700
+
701
+ self._subslice = False
702
+ if (self.axes
703
+ and len(x) > self._subslice_optim_min_size
704
+ and _path.is_sorted_and_has_non_nan(x)
705
+ and self.axes.name == 'rectilinear'
706
+ and self.axes.get_xscale() == 'linear'
707
+ and self._markevery is None
708
+ and self.get_clip_on()
709
+ and self.get_transform() == self.axes.transData):
710
+ self._subslice = True
711
+ nanmask = np.isnan(x)
712
+ if nanmask.any():
713
+ self._x_filled = self._x.copy()
714
+ indices = np.arange(len(x))
715
+ self._x_filled[nanmask] = np.interp(
716
+ indices[nanmask], indices[~nanmask], self._x[~nanmask])
717
+ else:
718
+ self._x_filled = self._x
719
+
720
+ if self._path is not None:
721
+ interpolation_steps = self._path._interpolation_steps
722
+ else:
723
+ interpolation_steps = 1
724
+ xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy.T)
725
+ self._path = Path(np.asarray(xy).T,
726
+ _interpolation_steps=interpolation_steps)
727
+ self._transformed_path = None
728
+ self._invalidx = False
729
+ self._invalidy = False
730
+
731
+ def _transform_path(self, subslice=None):
732
+ """
733
+ Put a TransformedPath instance at self._transformed_path;
734
+ all invalidation of the transform is then handled by the
735
+ TransformedPath instance.
736
+ """
737
+ # Masked arrays are now handled by the Path class itself
738
+ if subslice is not None:
739
+ xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy[subslice, :].T)
740
+ _path = Path(np.asarray(xy).T,
741
+ _interpolation_steps=self._path._interpolation_steps)
742
+ else:
743
+ _path = self._path
744
+ self._transformed_path = TransformedPath(_path, self.get_transform())
745
+
746
+ def _get_transformed_path(self):
747
+ """Return this line's `~matplotlib.transforms.TransformedPath`."""
748
+ if self._transformed_path is None:
749
+ self._transform_path()
750
+ return self._transformed_path
751
+
752
+ def set_transform(self, t):
753
+ # docstring inherited
754
+ self._invalidx = True
755
+ self._invalidy = True
756
+ super().set_transform(t)
757
+
758
+ @allow_rasterization
759
+ def draw(self, renderer):
760
+ # docstring inherited
761
+
762
+ if not self.get_visible():
763
+ return
764
+
765
+ if self._invalidy or self._invalidx:
766
+ self.recache()
767
+ self.ind_offset = 0 # Needed for contains() method.
768
+ if self._subslice and self.axes:
769
+ x0, x1 = self.axes.get_xbound()
770
+ i0 = self._x_filled.searchsorted(x0, 'left')
771
+ i1 = self._x_filled.searchsorted(x1, 'right')
772
+ subslice = slice(max(i0 - 1, 0), i1 + 1)
773
+ self.ind_offset = subslice.start
774
+ self._transform_path(subslice)
775
+ else:
776
+ subslice = None
777
+
778
+ if self.get_path_effects():
779
+ from matplotlib.patheffects import PathEffectRenderer
780
+ renderer = PathEffectRenderer(self.get_path_effects(), renderer)
781
+
782
+ renderer.open_group('line2d', self.get_gid())
783
+ if self._lineStyles[self._linestyle] != '_draw_nothing':
784
+ tpath, affine = (self._get_transformed_path()
785
+ .get_transformed_path_and_affine())
786
+ if len(tpath.vertices):
787
+ gc = renderer.new_gc()
788
+ self._set_gc_clip(gc)
789
+ gc.set_url(self.get_url())
790
+
791
+ gc.set_antialiased(self._antialiased)
792
+ gc.set_linewidth(self._linewidth)
793
+
794
+ if self.is_dashed():
795
+ cap = self._dashcapstyle
796
+ join = self._dashjoinstyle
797
+ else:
798
+ cap = self._solidcapstyle
799
+ join = self._solidjoinstyle
800
+ gc.set_joinstyle(join)
801
+ gc.set_capstyle(cap)
802
+ gc.set_snap(self.get_snap())
803
+ if self.get_sketch_params() is not None:
804
+ gc.set_sketch_params(*self.get_sketch_params())
805
+
806
+ # We first draw a path within the gaps if needed.
807
+ if self.is_dashed() and self._gapcolor is not None:
808
+ lc_rgba = mcolors.to_rgba(self._gapcolor, self._alpha)
809
+ gc.set_foreground(lc_rgba, isRGBA=True)
810
+
811
+ offset_gaps, gaps = _get_inverse_dash_pattern(
812
+ *self._dash_pattern)
813
+
814
+ gc.set_dashes(offset_gaps, gaps)
815
+ renderer.draw_path(gc, tpath, affine.frozen())
816
+
817
+ lc_rgba = mcolors.to_rgba(self._color, self._alpha)
818
+ gc.set_foreground(lc_rgba, isRGBA=True)
819
+
820
+ gc.set_dashes(*self._dash_pattern)
821
+ renderer.draw_path(gc, tpath, affine.frozen())
822
+ gc.restore()
823
+
824
+ if self._marker and self._markersize > 0:
825
+ gc = renderer.new_gc()
826
+ self._set_gc_clip(gc)
827
+ gc.set_url(self.get_url())
828
+ gc.set_linewidth(self._markeredgewidth)
829
+ gc.set_antialiased(self._antialiased)
830
+
831
+ ec_rgba = mcolors.to_rgba(
832
+ self.get_markeredgecolor(), self._alpha)
833
+ fc_rgba = mcolors.to_rgba(
834
+ self._get_markerfacecolor(), self._alpha)
835
+ fcalt_rgba = mcolors.to_rgba(
836
+ self._get_markerfacecolor(alt=True), self._alpha)
837
+ # If the edgecolor is "auto", it is set according to the *line*
838
+ # color but inherits the alpha value of the *face* color, if any.
839
+ if (cbook._str_equal(self._markeredgecolor, "auto")
840
+ and not cbook._str_lower_equal(
841
+ self.get_markerfacecolor(), "none")):
842
+ ec_rgba = ec_rgba[:3] + (fc_rgba[3],)
843
+ gc.set_foreground(ec_rgba, isRGBA=True)
844
+ if self.get_sketch_params() is not None:
845
+ scale, length, randomness = self.get_sketch_params()
846
+ gc.set_sketch_params(scale/2, length/2, 2*randomness)
847
+
848
+ marker = self._marker
849
+
850
+ # Markers *must* be drawn ignoring the drawstyle (but don't pay the
851
+ # recaching if drawstyle is already "default").
852
+ if self.get_drawstyle() != "default":
853
+ with cbook._setattr_cm(
854
+ self, _drawstyle="default", _transformed_path=None):
855
+ self.recache()
856
+ self._transform_path(subslice)
857
+ tpath, affine = (self._get_transformed_path()
858
+ .get_transformed_points_and_affine())
859
+ else:
860
+ tpath, affine = (self._get_transformed_path()
861
+ .get_transformed_points_and_affine())
862
+
863
+ if len(tpath.vertices):
864
+ # subsample the markers if markevery is not None
865
+ markevery = self.get_markevery()
866
+ if markevery is not None:
867
+ subsampled = _mark_every_path(
868
+ markevery, tpath, affine, self.axes)
869
+ else:
870
+ subsampled = tpath
871
+
872
+ snap = marker.get_snap_threshold()
873
+ if isinstance(snap, Real):
874
+ snap = renderer.points_to_pixels(self._markersize) >= snap
875
+ gc.set_snap(snap)
876
+ gc.set_joinstyle(marker.get_joinstyle())
877
+ gc.set_capstyle(marker.get_capstyle())
878
+ marker_path = marker.get_path()
879
+ marker_trans = marker.get_transform()
880
+ w = renderer.points_to_pixels(self._markersize)
881
+
882
+ if cbook._str_equal(marker.get_marker(), ","):
883
+ gc.set_linewidth(0)
884
+ else:
885
+ # Don't scale for pixels, and don't stroke them
886
+ marker_trans = marker_trans.scale(w)
887
+ renderer.draw_markers(gc, marker_path, marker_trans,
888
+ subsampled, affine.frozen(),
889
+ fc_rgba)
890
+
891
+ alt_marker_path = marker.get_alt_path()
892
+ if alt_marker_path:
893
+ alt_marker_trans = marker.get_alt_transform()
894
+ alt_marker_trans = alt_marker_trans.scale(w)
895
+ renderer.draw_markers(
896
+ gc, alt_marker_path, alt_marker_trans, subsampled,
897
+ affine.frozen(), fcalt_rgba)
898
+
899
+ gc.restore()
900
+
901
+ renderer.close_group('line2d')
902
+ self.stale = False
903
+
904
+ def get_antialiased(self):
905
+ """Return whether antialiased rendering is used."""
906
+ return self._antialiased
907
+
908
+ def get_color(self):
909
+ """
910
+ Return the line color.
911
+
912
+ See also `~.Line2D.set_color`.
913
+ """
914
+ return self._color
915
+
916
+ def get_drawstyle(self):
917
+ """
918
+ Return the drawstyle.
919
+
920
+ See also `~.Line2D.set_drawstyle`.
921
+ """
922
+ return self._drawstyle
923
+
924
+ def get_gapcolor(self):
925
+ """
926
+ Return the line gapcolor.
927
+
928
+ See also `~.Line2D.set_gapcolor`.
929
+ """
930
+ return self._gapcolor
931
+
932
+ def get_linestyle(self):
933
+ """
934
+ Return the linestyle.
935
+
936
+ See also `~.Line2D.set_linestyle`.
937
+ """
938
+ return self._linestyle
939
+
940
+ def get_linewidth(self):
941
+ """
942
+ Return the linewidth in points.
943
+
944
+ See also `~.Line2D.set_linewidth`.
945
+ """
946
+ return self._linewidth
947
+
948
+ def get_marker(self):
949
+ """
950
+ Return the line marker.
951
+
952
+ See also `~.Line2D.set_marker`.
953
+ """
954
+ return self._marker.get_marker()
955
+
956
+ def get_markeredgecolor(self):
957
+ """
958
+ Return the marker edge color.
959
+
960
+ See also `~.Line2D.set_markeredgecolor`.
961
+ """
962
+ mec = self._markeredgecolor
963
+ if cbook._str_equal(mec, 'auto'):
964
+ if mpl.rcParams['_internal.classic_mode']:
965
+ if self._marker.get_marker() in ('.', ','):
966
+ return self._color
967
+ if (self._marker.is_filled()
968
+ and self._marker.get_fillstyle() != 'none'):
969
+ return 'k' # Bad hard-wired default...
970
+ return self._color
971
+ else:
972
+ return mec
973
+
974
+ def get_markeredgewidth(self):
975
+ """
976
+ Return the marker edge width in points.
977
+
978
+ See also `~.Line2D.set_markeredgewidth`.
979
+ """
980
+ return self._markeredgewidth
981
+
982
+ def _get_markerfacecolor(self, alt=False):
983
+ if self._marker.get_fillstyle() == 'none':
984
+ return 'none'
985
+ fc = self._markerfacecoloralt if alt else self._markerfacecolor
986
+ if cbook._str_lower_equal(fc, 'auto'):
987
+ return self._color
988
+ else:
989
+ return fc
990
+
991
+ def get_markerfacecolor(self):
992
+ """
993
+ Return the marker face color.
994
+
995
+ See also `~.Line2D.set_markerfacecolor`.
996
+ """
997
+ return self._get_markerfacecolor(alt=False)
998
+
999
+ def get_markerfacecoloralt(self):
1000
+ """
1001
+ Return the alternate marker face color.
1002
+
1003
+ See also `~.Line2D.set_markerfacecoloralt`.
1004
+ """
1005
+ return self._get_markerfacecolor(alt=True)
1006
+
1007
+ def get_markersize(self):
1008
+ """
1009
+ Return the marker size in points.
1010
+
1011
+ See also `~.Line2D.set_markersize`.
1012
+ """
1013
+ return self._markersize
1014
+
1015
+ def get_data(self, orig=True):
1016
+ """
1017
+ Return the line data as an ``(xdata, ydata)`` pair.
1018
+
1019
+ If *orig* is *True*, return the original data.
1020
+ """
1021
+ return self.get_xdata(orig=orig), self.get_ydata(orig=orig)
1022
+
1023
+ def get_xdata(self, orig=True):
1024
+ """
1025
+ Return the xdata.
1026
+
1027
+ If *orig* is *True*, return the original data, else the
1028
+ processed data.
1029
+ """
1030
+ if orig:
1031
+ return self._xorig
1032
+ if self._invalidx:
1033
+ self.recache()
1034
+ return self._x
1035
+
1036
+ def get_ydata(self, orig=True):
1037
+ """
1038
+ Return the ydata.
1039
+
1040
+ If *orig* is *True*, return the original data, else the
1041
+ processed data.
1042
+ """
1043
+ if orig:
1044
+ return self._yorig
1045
+ if self._invalidy:
1046
+ self.recache()
1047
+ return self._y
1048
+
1049
+ def get_path(self):
1050
+ """Return the `~matplotlib.path.Path` associated with this line."""
1051
+ if self._invalidy or self._invalidx:
1052
+ self.recache()
1053
+ return self._path
1054
+
1055
+ def get_xydata(self):
1056
+ """Return the *xy* data as a (N, 2) array."""
1057
+ if self._invalidy or self._invalidx:
1058
+ self.recache()
1059
+ return self._xy
1060
+
1061
+ def set_antialiased(self, b):
1062
+ """
1063
+ Set whether to use antialiased rendering.
1064
+
1065
+ Parameters
1066
+ ----------
1067
+ b : bool
1068
+ """
1069
+ if self._antialiased != b:
1070
+ self.stale = True
1071
+ self._antialiased = b
1072
+
1073
+ def set_color(self, color):
1074
+ """
1075
+ Set the color of the line.
1076
+
1077
+ Parameters
1078
+ ----------
1079
+ color : :mpltype:`color`
1080
+ """
1081
+ mcolors._check_color_like(color=color)
1082
+ self._color = color
1083
+ self.stale = True
1084
+
1085
+ def set_drawstyle(self, drawstyle):
1086
+ """
1087
+ Set the drawstyle of the plot.
1088
+
1089
+ The drawstyle determines how the points are connected.
1090
+
1091
+ Parameters
1092
+ ----------
1093
+ drawstyle : {'default', 'steps', 'steps-pre', 'steps-mid', \
1094
+ 'steps-post'}, default: 'default'
1095
+ For 'default', the points are connected with straight lines.
1096
+
1097
+ The steps variants connect the points with step-like lines,
1098
+ i.e. horizontal lines with vertical steps. They differ in the
1099
+ location of the step:
1100
+
1101
+ - 'steps-pre': The step is at the beginning of the line segment,
1102
+ i.e. the line will be at the y-value of point to the right.
1103
+ - 'steps-mid': The step is halfway between the points.
1104
+ - 'steps-post: The step is at the end of the line segment,
1105
+ i.e. the line will be at the y-value of the point to the left.
1106
+ - 'steps' is equal to 'steps-pre' and is maintained for
1107
+ backward-compatibility.
1108
+
1109
+ For examples see :doc:`/gallery/lines_bars_and_markers/step_demo`.
1110
+ """
1111
+ if drawstyle is None:
1112
+ drawstyle = 'default'
1113
+ _api.check_in_list(self.drawStyles, drawstyle=drawstyle)
1114
+ if self._drawstyle != drawstyle:
1115
+ self.stale = True
1116
+ # invalidate to trigger a recache of the path
1117
+ self._invalidx = True
1118
+ self._drawstyle = drawstyle
1119
+
1120
+ def set_gapcolor(self, gapcolor):
1121
+ """
1122
+ Set a color to fill the gaps in the dashed line style.
1123
+
1124
+ .. note::
1125
+
1126
+ Striped lines are created by drawing two interleaved dashed lines.
1127
+ There can be overlaps between those two, which may result in
1128
+ artifacts when using transparency.
1129
+
1130
+ This functionality is experimental and may change.
1131
+
1132
+ Parameters
1133
+ ----------
1134
+ gapcolor : :mpltype:`color` or None
1135
+ The color with which to fill the gaps. If None, the gaps are
1136
+ unfilled.
1137
+ """
1138
+ if gapcolor is not None:
1139
+ mcolors._check_color_like(color=gapcolor)
1140
+ self._gapcolor = gapcolor
1141
+ self.stale = True
1142
+
1143
+ def set_linewidth(self, w):
1144
+ """
1145
+ Set the line width in points.
1146
+
1147
+ Parameters
1148
+ ----------
1149
+ w : float
1150
+ Line width, in points.
1151
+ """
1152
+ w = float(w)
1153
+ if self._linewidth != w:
1154
+ self.stale = True
1155
+ self._linewidth = w
1156
+ self._dash_pattern = _scale_dashes(*self._unscaled_dash_pattern, w)
1157
+
1158
+ def set_linestyle(self, ls):
1159
+ """
1160
+ Set the linestyle of the line.
1161
+
1162
+ Parameters
1163
+ ----------
1164
+ ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
1165
+ Possible values:
1166
+
1167
+ - A string:
1168
+
1169
+ ========================================== =================
1170
+ linestyle description
1171
+ ========================================== =================
1172
+ ``'-'`` or ``'solid'`` solid line
1173
+ ``'--'`` or ``'dashed'`` dashed line
1174
+ ``'-.'`` or ``'dashdot'`` dash-dotted line
1175
+ ``':'`` or ``'dotted'`` dotted line
1176
+ ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing
1177
+ ========================================== =================
1178
+
1179
+ - Alternatively a dash tuple of the following form can be
1180
+ provided::
1181
+
1182
+ (offset, onoffseq)
1183
+
1184
+ where ``onoffseq`` is an even length tuple of on and off ink
1185
+ in points. See also :meth:`set_dashes`.
1186
+
1187
+ For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`.
1188
+ """
1189
+ if isinstance(ls, str):
1190
+ if ls in [' ', '', 'none']:
1191
+ ls = 'None'
1192
+ _api.check_in_list([*self._lineStyles, *ls_mapper_r], ls=ls)
1193
+ if ls not in self._lineStyles:
1194
+ ls = ls_mapper_r[ls]
1195
+ self._linestyle = ls
1196
+ else:
1197
+ self._linestyle = '--'
1198
+ self._unscaled_dash_pattern = _get_dash_pattern(ls)
1199
+ self._dash_pattern = _scale_dashes(
1200
+ *self._unscaled_dash_pattern, self._linewidth)
1201
+ self.stale = True
1202
+
1203
+ @_docstring.interpd
1204
+ def set_marker(self, marker):
1205
+ """
1206
+ Set the line marker.
1207
+
1208
+ Parameters
1209
+ ----------
1210
+ marker : marker style string, `~.path.Path` or `~.markers.MarkerStyle`
1211
+ See `~matplotlib.markers` for full description of possible
1212
+ arguments.
1213
+ """
1214
+ self._marker = MarkerStyle(marker, self._marker.get_fillstyle())
1215
+ self.stale = True
1216
+
1217
+ def _set_markercolor(self, name, has_rcdefault, val):
1218
+ if val is None:
1219
+ val = mpl.rcParams[f"lines.{name}"] if has_rcdefault else "auto"
1220
+ attr = f"_{name}"
1221
+ current = getattr(self, attr)
1222
+ if current is None:
1223
+ self.stale = True
1224
+ else:
1225
+ neq = current != val
1226
+ # Much faster than `np.any(current != val)` if no arrays are used.
1227
+ if neq.any() if isinstance(neq, np.ndarray) else neq:
1228
+ self.stale = True
1229
+ setattr(self, attr, val)
1230
+
1231
+ def set_markeredgecolor(self, ec):
1232
+ """
1233
+ Set the marker edge color.
1234
+
1235
+ Parameters
1236
+ ----------
1237
+ ec : :mpltype:`color`
1238
+ """
1239
+ self._set_markercolor("markeredgecolor", True, ec)
1240
+
1241
+ def set_markerfacecolor(self, fc):
1242
+ """
1243
+ Set the marker face color.
1244
+
1245
+ Parameters
1246
+ ----------
1247
+ fc : :mpltype:`color`
1248
+ """
1249
+ self._set_markercolor("markerfacecolor", True, fc)
1250
+
1251
+ def set_markerfacecoloralt(self, fc):
1252
+ """
1253
+ Set the alternate marker face color.
1254
+
1255
+ Parameters
1256
+ ----------
1257
+ fc : :mpltype:`color`
1258
+ """
1259
+ self._set_markercolor("markerfacecoloralt", False, fc)
1260
+
1261
+ def set_markeredgewidth(self, ew):
1262
+ """
1263
+ Set the marker edge width in points.
1264
+
1265
+ Parameters
1266
+ ----------
1267
+ ew : float
1268
+ Marker edge width, in points.
1269
+ """
1270
+ if ew is None:
1271
+ ew = mpl.rcParams['lines.markeredgewidth']
1272
+ if self._markeredgewidth != ew:
1273
+ self.stale = True
1274
+ self._markeredgewidth = ew
1275
+
1276
+ def set_markersize(self, sz):
1277
+ """
1278
+ Set the marker size in points.
1279
+
1280
+ Parameters
1281
+ ----------
1282
+ sz : float
1283
+ Marker size, in points.
1284
+ """
1285
+ sz = float(sz)
1286
+ if self._markersize != sz:
1287
+ self.stale = True
1288
+ self._markersize = sz
1289
+
1290
+ def set_xdata(self, x):
1291
+ """
1292
+ Set the data array for x.
1293
+
1294
+ Parameters
1295
+ ----------
1296
+ x : 1D array
1297
+
1298
+ See Also
1299
+ --------
1300
+ set_data
1301
+ set_ydata
1302
+ """
1303
+ if not np.iterable(x):
1304
+ raise RuntimeError('x must be a sequence')
1305
+ self._xorig = copy.copy(x)
1306
+ self._invalidx = True
1307
+ self.stale = True
1308
+
1309
+ def set_ydata(self, y):
1310
+ """
1311
+ Set the data array for y.
1312
+
1313
+ Parameters
1314
+ ----------
1315
+ y : 1D array
1316
+
1317
+ See Also
1318
+ --------
1319
+ set_data
1320
+ set_xdata
1321
+ """
1322
+ if not np.iterable(y):
1323
+ raise RuntimeError('y must be a sequence')
1324
+ self._yorig = copy.copy(y)
1325
+ self._invalidy = True
1326
+ self.stale = True
1327
+
1328
+ def set_dashes(self, seq):
1329
+ """
1330
+ Set the dash sequence.
1331
+
1332
+ The dash sequence is a sequence of floats of even length describing
1333
+ the length of dashes and spaces in points.
1334
+
1335
+ For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point
1336
+ dashes separated by 2 point spaces.
1337
+
1338
+ See also `~.Line2D.set_gapcolor`, which allows those spaces to be
1339
+ filled with a color.
1340
+
1341
+ Parameters
1342
+ ----------
1343
+ seq : sequence of floats (on/off ink in points) or (None, None)
1344
+ If *seq* is empty or ``(None, None)``, the linestyle will be set
1345
+ to solid.
1346
+ """
1347
+ if seq == (None, None) or len(seq) == 0:
1348
+ self.set_linestyle('-')
1349
+ else:
1350
+ self.set_linestyle((0, seq))
1351
+
1352
+ def update_from(self, other):
1353
+ """Copy properties from *other* to self."""
1354
+ super().update_from(other)
1355
+ self._linestyle = other._linestyle
1356
+ self._linewidth = other._linewidth
1357
+ self._color = other._color
1358
+ self._gapcolor = other._gapcolor
1359
+ self._markersize = other._markersize
1360
+ self._markerfacecolor = other._markerfacecolor
1361
+ self._markerfacecoloralt = other._markerfacecoloralt
1362
+ self._markeredgecolor = other._markeredgecolor
1363
+ self._markeredgewidth = other._markeredgewidth
1364
+ self._unscaled_dash_pattern = other._unscaled_dash_pattern
1365
+ self._dash_pattern = other._dash_pattern
1366
+ self._dashcapstyle = other._dashcapstyle
1367
+ self._dashjoinstyle = other._dashjoinstyle
1368
+ self._solidcapstyle = other._solidcapstyle
1369
+ self._solidjoinstyle = other._solidjoinstyle
1370
+
1371
+ self._linestyle = other._linestyle
1372
+ self._marker = MarkerStyle(marker=other._marker)
1373
+ self._drawstyle = other._drawstyle
1374
+
1375
+ @_docstring.interpd
1376
+ def set_dash_joinstyle(self, s):
1377
+ """
1378
+ How to join segments of the line if it `~Line2D.is_dashed`.
1379
+
1380
+ The default joinstyle is :rc:`lines.dash_joinstyle`.
1381
+
1382
+ Parameters
1383
+ ----------
1384
+ s : `.JoinStyle` or %(JoinStyle)s
1385
+ """
1386
+ js = JoinStyle(s)
1387
+ if self._dashjoinstyle != js:
1388
+ self.stale = True
1389
+ self._dashjoinstyle = js
1390
+
1391
+ @_docstring.interpd
1392
+ def set_solid_joinstyle(self, s):
1393
+ """
1394
+ How to join segments if the line is solid (not `~Line2D.is_dashed`).
1395
+
1396
+ The default joinstyle is :rc:`lines.solid_joinstyle`.
1397
+
1398
+ Parameters
1399
+ ----------
1400
+ s : `.JoinStyle` or %(JoinStyle)s
1401
+ """
1402
+ js = JoinStyle(s)
1403
+ if self._solidjoinstyle != js:
1404
+ self.stale = True
1405
+ self._solidjoinstyle = js
1406
+
1407
+ def get_dash_joinstyle(self):
1408
+ """
1409
+ Return the `.JoinStyle` for dashed lines.
1410
+
1411
+ See also `~.Line2D.set_dash_joinstyle`.
1412
+ """
1413
+ return self._dashjoinstyle.name
1414
+
1415
+ def get_solid_joinstyle(self):
1416
+ """
1417
+ Return the `.JoinStyle` for solid lines.
1418
+
1419
+ See also `~.Line2D.set_solid_joinstyle`.
1420
+ """
1421
+ return self._solidjoinstyle.name
1422
+
1423
+ @_docstring.interpd
1424
+ def set_dash_capstyle(self, s):
1425
+ """
1426
+ How to draw the end caps if the line is `~Line2D.is_dashed`.
1427
+
1428
+ The default capstyle is :rc:`lines.dash_capstyle`.
1429
+
1430
+ Parameters
1431
+ ----------
1432
+ s : `.CapStyle` or %(CapStyle)s
1433
+ """
1434
+ cs = CapStyle(s)
1435
+ if self._dashcapstyle != cs:
1436
+ self.stale = True
1437
+ self._dashcapstyle = cs
1438
+
1439
+ @_docstring.interpd
1440
+ def set_solid_capstyle(self, s):
1441
+ """
1442
+ How to draw the end caps if the line is solid (not `~Line2D.is_dashed`)
1443
+
1444
+ The default capstyle is :rc:`lines.solid_capstyle`.
1445
+
1446
+ Parameters
1447
+ ----------
1448
+ s : `.CapStyle` or %(CapStyle)s
1449
+ """
1450
+ cs = CapStyle(s)
1451
+ if self._solidcapstyle != cs:
1452
+ self.stale = True
1453
+ self._solidcapstyle = cs
1454
+
1455
+ def get_dash_capstyle(self):
1456
+ """
1457
+ Return the `.CapStyle` for dashed lines.
1458
+
1459
+ See also `~.Line2D.set_dash_capstyle`.
1460
+ """
1461
+ return self._dashcapstyle.name
1462
+
1463
+ def get_solid_capstyle(self):
1464
+ """
1465
+ Return the `.CapStyle` for solid lines.
1466
+
1467
+ See also `~.Line2D.set_solid_capstyle`.
1468
+ """
1469
+ return self._solidcapstyle.name
1470
+
1471
+ def is_dashed(self):
1472
+ """
1473
+ Return whether line has a dashed linestyle.
1474
+
1475
+ A custom linestyle is assumed to be dashed, we do not inspect the
1476
+ ``onoffseq`` directly.
1477
+
1478
+ See also `~.Line2D.set_linestyle`.
1479
+ """
1480
+ return self._linestyle in ('--', '-.', ':')
1481
+
1482
+
1483
+ class AxLine(Line2D):
1484
+ """
1485
+ A helper class that implements `~.Axes.axline`, by recomputing the artist
1486
+ transform at draw time.
1487
+ """
1488
+
1489
+ def __init__(self, xy1, xy2, slope, **kwargs):
1490
+ """
1491
+ Parameters
1492
+ ----------
1493
+ xy1 : (float, float)
1494
+ The first set of (x, y) coordinates for the line to pass through.
1495
+ xy2 : (float, float) or None
1496
+ The second set of (x, y) coordinates for the line to pass through.
1497
+ Both *xy2* and *slope* must be passed, but one of them must be None.
1498
+ slope : float or None
1499
+ The slope of the line. Both *xy2* and *slope* must be passed, but one of
1500
+ them must be None.
1501
+ """
1502
+ super().__init__([0, 1], [0, 1], **kwargs)
1503
+
1504
+ if (xy2 is None and slope is None or
1505
+ xy2 is not None and slope is not None):
1506
+ raise TypeError(
1507
+ "Exactly one of 'xy2' and 'slope' must be given")
1508
+
1509
+ self._slope = slope
1510
+ self._xy1 = xy1
1511
+ self._xy2 = xy2
1512
+
1513
+ def get_transform(self):
1514
+ ax = self.axes
1515
+ points_transform = self._transform - ax.transData + ax.transScale
1516
+
1517
+ if self._xy2 is not None:
1518
+ # two points were given
1519
+ (x1, y1), (x2, y2) = \
1520
+ points_transform.transform([self._xy1, self._xy2])
1521
+ dx = x2 - x1
1522
+ dy = y2 - y1
1523
+ if dx == 0:
1524
+ if dy == 0:
1525
+ raise ValueError(
1526
+ f"Cannot draw a line through two identical points "
1527
+ f"(x={(x1, x2)}, y={(y1, y2)})")
1528
+ slope = np.inf
1529
+ else:
1530
+ slope = dy / dx
1531
+ else:
1532
+ # one point and a slope were given
1533
+ x1, y1 = points_transform.transform(self._xy1)
1534
+ slope = self._slope
1535
+ (vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim)
1536
+ # General case: find intersections with view limits in either
1537
+ # direction, and draw between the middle two points.
1538
+ if slope == 0:
1539
+ start = vxlo, y1
1540
+ stop = vxhi, y1
1541
+ elif np.isinf(slope):
1542
+ start = x1, vylo
1543
+ stop = x1, vyhi
1544
+ else:
1545
+ _, start, stop, _ = sorted([
1546
+ (vxlo, y1 + (vxlo - x1) * slope),
1547
+ (vxhi, y1 + (vxhi - x1) * slope),
1548
+ (x1 + (vylo - y1) / slope, vylo),
1549
+ (x1 + (vyhi - y1) / slope, vyhi),
1550
+ ])
1551
+ return (BboxTransformTo(Bbox([start, stop]))
1552
+ + ax.transLimits + ax.transAxes)
1553
+
1554
+ def draw(self, renderer):
1555
+ self._transformed_path = None # Force regen.
1556
+ super().draw(renderer)
1557
+
1558
+ def get_xy1(self):
1559
+ """Return the *xy1* value of the line."""
1560
+ return self._xy1
1561
+
1562
+ def get_xy2(self):
1563
+ """Return the *xy2* value of the line."""
1564
+ return self._xy2
1565
+
1566
+ def get_slope(self):
1567
+ """Return the *slope* value of the line."""
1568
+ return self._slope
1569
+
1570
+ def set_xy1(self, *args, **kwargs):
1571
+ """
1572
+ Set the *xy1* value of the line.
1573
+
1574
+ Parameters
1575
+ ----------
1576
+ xy1 : tuple[float, float]
1577
+ Points for the line to pass through.
1578
+ """
1579
+ params = _api.select_matching_signature([
1580
+ lambda self, x, y: locals(), lambda self, xy1: locals(),
1581
+ ], self, *args, **kwargs)
1582
+ if "x" in params:
1583
+ _api.warn_deprecated("3.10", message=(
1584
+ "Passing x and y separately to AxLine.set_xy1 is deprecated since "
1585
+ "%(since)s; pass them as a single tuple instead."))
1586
+ xy1 = params["x"], params["y"]
1587
+ else:
1588
+ xy1 = params["xy1"]
1589
+ self._xy1 = xy1
1590
+
1591
+ def set_xy2(self, *args, **kwargs):
1592
+ """
1593
+ Set the *xy2* value of the line.
1594
+
1595
+ .. note::
1596
+
1597
+ You can only set *xy2* if the line was created using the *xy2*
1598
+ parameter. If the line was created using *slope*, please use
1599
+ `~.AxLine.set_slope`.
1600
+
1601
+ Parameters
1602
+ ----------
1603
+ xy2 : tuple[float, float]
1604
+ Points for the line to pass through.
1605
+ """
1606
+ if self._slope is None:
1607
+ params = _api.select_matching_signature([
1608
+ lambda self, x, y: locals(), lambda self, xy2: locals(),
1609
+ ], self, *args, **kwargs)
1610
+ if "x" in params:
1611
+ _api.warn_deprecated("3.10", message=(
1612
+ "Passing x and y separately to AxLine.set_xy2 is deprecated since "
1613
+ "%(since)s; pass them as a single tuple instead."))
1614
+ xy2 = params["x"], params["y"]
1615
+ else:
1616
+ xy2 = params["xy2"]
1617
+ self._xy2 = xy2
1618
+ else:
1619
+ raise ValueError("Cannot set an 'xy2' value while 'slope' is set;"
1620
+ " they differ but their functionalities overlap")
1621
+
1622
+ def set_slope(self, slope):
1623
+ """
1624
+ Set the *slope* value of the line.
1625
+
1626
+ .. note::
1627
+
1628
+ You can only set *slope* if the line was created using the *slope*
1629
+ parameter. If the line was created using *xy2*, please use
1630
+ `~.AxLine.set_xy2`.
1631
+
1632
+ Parameters
1633
+ ----------
1634
+ slope : float
1635
+ The slope of the line.
1636
+ """
1637
+ if self._xy2 is None:
1638
+ self._slope = slope
1639
+ else:
1640
+ raise ValueError("Cannot set a 'slope' value while 'xy2' is set;"
1641
+ " they differ but their functionalities overlap")
1642
+
1643
+
1644
+ class VertexSelector:
1645
+ """
1646
+ Manage the callbacks to maintain a list of selected vertices for `.Line2D`.
1647
+ Derived classes should override the `process_selected` method to do
1648
+ something with the picks.
1649
+
1650
+ Here is an example which highlights the selected verts with red circles::
1651
+
1652
+ import numpy as np
1653
+ import matplotlib.pyplot as plt
1654
+ import matplotlib.lines as lines
1655
+
1656
+ class HighlightSelected(lines.VertexSelector):
1657
+ def __init__(self, line, fmt='ro', **kwargs):
1658
+ super().__init__(line)
1659
+ self.markers, = self.axes.plot([], [], fmt, **kwargs)
1660
+
1661
+ def process_selected(self, ind, xs, ys):
1662
+ self.markers.set_data(xs, ys)
1663
+ self.canvas.draw()
1664
+
1665
+ fig, ax = plt.subplots()
1666
+ x, y = np.random.rand(2, 30)
1667
+ line, = ax.plot(x, y, 'bs-', picker=5)
1668
+
1669
+ selector = HighlightSelected(line)
1670
+ plt.show()
1671
+ """
1672
+
1673
+ def __init__(self, line):
1674
+ """
1675
+ Parameters
1676
+ ----------
1677
+ line : `~matplotlib.lines.Line2D`
1678
+ The line must already have been added to an `~.axes.Axes` and must
1679
+ have its picker property set.
1680
+ """
1681
+ if line.axes is None:
1682
+ raise RuntimeError('You must first add the line to the Axes')
1683
+ if line.get_picker() is None:
1684
+ raise RuntimeError('You must first set the picker property '
1685
+ 'of the line')
1686
+ self.axes = line.axes
1687
+ self.line = line
1688
+ self.cid = self.canvas.callbacks._connect_picklable(
1689
+ 'pick_event', self.onpick)
1690
+ self.ind = set()
1691
+
1692
+ canvas = property(lambda self: self.axes.get_figure(root=True).canvas)
1693
+
1694
+ def process_selected(self, ind, xs, ys):
1695
+ """
1696
+ Default "do nothing" implementation of the `process_selected` method.
1697
+
1698
+ Parameters
1699
+ ----------
1700
+ ind : list of int
1701
+ The indices of the selected vertices.
1702
+ xs, ys : array-like
1703
+ The coordinates of the selected vertices.
1704
+ """
1705
+ pass
1706
+
1707
+ def onpick(self, event):
1708
+ """When the line is picked, update the set of selected indices."""
1709
+ if event.artist is not self.line:
1710
+ return
1711
+ self.ind ^= set(event.ind)
1712
+ ind = sorted(self.ind)
1713
+ xdata, ydata = self.line.get_data()
1714
+ self.process_selected(ind, xdata[ind], ydata[ind])
1715
+
1716
+
1717
+ lineStyles = Line2D._lineStyles
1718
+ lineMarkers = MarkerStyle.markers
1719
+ drawStyles = Line2D.drawStyles
1720
+ fillStyles = MarkerStyle.fillstyles
lib/python3.12/site-packages/matplotlib/mathtext.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""
2
+ A module for parsing a subset of the TeX math syntax and rendering it to a
3
+ Matplotlib backend.
4
+
5
+ For a tutorial of its usage, see :ref:`mathtext`. This
6
+ document is primarily concerned with implementation details.
7
+
8
+ The module uses pyparsing_ to parse the TeX expression.
9
+
10
+ .. _pyparsing: https://pypi.org/project/pyparsing/
11
+
12
+ The Bakoma distribution of the TeX Computer Modern fonts, and STIX
13
+ fonts are supported. There is experimental support for using
14
+ arbitrary fonts, but results may vary without proper tweaking and
15
+ metrics for those fonts.
16
+ """
17
+
18
+ import functools
19
+ import logging
20
+
21
+ import matplotlib as mpl
22
+ from matplotlib import _api, _mathtext
23
+ from matplotlib.ft2font import LoadFlags
24
+ from matplotlib.font_manager import FontProperties
25
+ from ._mathtext import ( # noqa: F401, reexported API
26
+ RasterParse, VectorParse, get_unicode_index)
27
+
28
+ _log = logging.getLogger(__name__)
29
+
30
+
31
+ get_unicode_index.__module__ = __name__
32
+
33
+ ##############################################################################
34
+ # MAIN
35
+
36
+
37
+ class MathTextParser:
38
+ _parser = None
39
+ _font_type_mapping = {
40
+ 'cm': _mathtext.BakomaFonts,
41
+ 'dejavuserif': _mathtext.DejaVuSerifFonts,
42
+ 'dejavusans': _mathtext.DejaVuSansFonts,
43
+ 'stix': _mathtext.StixFonts,
44
+ 'stixsans': _mathtext.StixSansFonts,
45
+ 'custom': _mathtext.UnicodeFonts,
46
+ }
47
+
48
+ def __init__(self, output):
49
+ """
50
+ Create a MathTextParser for the given backend *output*.
51
+
52
+ Parameters
53
+ ----------
54
+ output : {"path", "agg"}
55
+ Whether to return a `VectorParse` ("path") or a
56
+ `RasterParse` ("agg", or its synonym "macosx").
57
+ """
58
+ self._output_type = _api.check_getitem(
59
+ {"path": "vector", "agg": "raster", "macosx": "raster"},
60
+ output=output.lower())
61
+
62
+ def parse(self, s, dpi=72, prop=None, *, antialiased=None):
63
+ """
64
+ Parse the given math expression *s* at the given *dpi*. If *prop* is
65
+ provided, it is a `.FontProperties` object specifying the "default"
66
+ font to use in the math expression, used for all non-math text.
67
+
68
+ The results are cached, so multiple calls to `parse`
69
+ with the same expression should be fast.
70
+
71
+ Depending on the *output* type, this returns either a `VectorParse` or
72
+ a `RasterParse`.
73
+ """
74
+ # lru_cache can't decorate parse() directly because prop is
75
+ # mutable, so we key the cache using an internal copy (see
76
+ # Text._get_text_metrics_with_cache for a similar case); likewise,
77
+ # we need to check the mutable state of the text.antialiased and
78
+ # text.hinting rcParams.
79
+ prop = prop.copy() if prop is not None else None
80
+ antialiased = mpl._val_or_rc(antialiased, 'text.antialiased')
81
+ from matplotlib.backends import backend_agg
82
+ load_glyph_flags = {
83
+ "vector": LoadFlags.NO_HINTING,
84
+ "raster": backend_agg.get_hinting_flag(),
85
+ }[self._output_type]
86
+ return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
87
+
88
+ @functools.lru_cache(50)
89
+ def _parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags):
90
+ if prop is None:
91
+ prop = FontProperties()
92
+ fontset_class = _api.check_getitem(
93
+ self._font_type_mapping, fontset=prop.get_math_fontfamily())
94
+ fontset = fontset_class(prop, load_glyph_flags)
95
+ fontsize = prop.get_size_in_points()
96
+
97
+ if self._parser is None: # Cache the parser globally.
98
+ self.__class__._parser = _mathtext.Parser()
99
+
100
+ box = self._parser.parse(s, fontset, fontsize, dpi)
101
+ output = _mathtext.ship(box)
102
+ if self._output_type == "vector":
103
+ return output.to_vector()
104
+ elif self._output_type == "raster":
105
+ return output.to_raster(antialiased=antialiased)
106
+
107
+
108
+ def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None,
109
+ *, color=None):
110
+ """
111
+ Given a math expression, renders it in a closely-clipped bounding
112
+ box to an image file.
113
+
114
+ Parameters
115
+ ----------
116
+ s : str
117
+ A math expression. The math portion must be enclosed in dollar signs.
118
+ filename_or_obj : str or path-like or file-like
119
+ Where to write the image data.
120
+ prop : `.FontProperties`, optional
121
+ The size and style of the text.
122
+ dpi : float, optional
123
+ The output dpi. If not set, the dpi is determined as for
124
+ `.Figure.savefig`.
125
+ format : str, optional
126
+ The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the
127
+ format is determined as for `.Figure.savefig`.
128
+ color : str, optional
129
+ Foreground color, defaults to :rc:`text.color`.
130
+ """
131
+ from matplotlib import figure
132
+
133
+ parser = MathTextParser('path')
134
+ width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
135
+
136
+ fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
137
+ fig.text(0, depth/height, s, fontproperties=prop, color=color)
138
+ fig.savefig(filename_or_obj, dpi=dpi, format=format)
139
+
140
+ return depth
lib/python3.12/site-packages/matplotlib/offsetbox.pyi ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.artist as martist
2
+ from matplotlib.backend_bases import RendererBase, Event, FigureCanvasBase
3
+ from matplotlib.colors import Colormap, Normalize
4
+ import matplotlib.text as mtext
5
+ from matplotlib.figure import Figure, SubFigure
6
+ from matplotlib.font_manager import FontProperties
7
+ from matplotlib.image import BboxImage
8
+ from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
9
+ from matplotlib.transforms import Bbox, BboxBase, Transform
10
+ from matplotlib.typing import CoordsType
11
+
12
+ import numpy as np
13
+ from numpy.typing import ArrayLike
14
+ from collections.abc import Callable, Sequence
15
+ from typing import Any, Literal, overload
16
+
17
+ DEBUG: bool
18
+
19
+ def _get_packed_offsets(
20
+ widths: Sequence[float],
21
+ total: float | None,
22
+ sep: float | None,
23
+ mode: Literal["fixed", "expand", "equal"] = ...,
24
+ ) -> tuple[float, np.ndarray]: ...
25
+
26
+ class OffsetBox(martist.Artist):
27
+ width: float | None
28
+ height: float | None
29
+ def __init__(self, *args, **kwargs) -> None: ...
30
+ def set_figure(self, fig: Figure | SubFigure) -> None: ...
31
+ def set_offset(
32
+ self,
33
+ xy: tuple[float, float]
34
+ | Callable[[float, float, float, float, RendererBase], tuple[float, float]],
35
+ ) -> None: ...
36
+
37
+ @overload
38
+ def get_offset(self, bbox: Bbox, renderer: RendererBase) -> tuple[float, float]: ...
39
+ @overload
40
+ def get_offset(
41
+ self,
42
+ width: float,
43
+ height: float,
44
+ xdescent: float,
45
+ ydescent: float,
46
+ renderer: RendererBase
47
+ ) -> tuple[float, float]: ...
48
+
49
+ def set_width(self, width: float) -> None: ...
50
+ def set_height(self, height: float) -> None: ...
51
+ def get_visible_children(self) -> list[martist.Artist]: ...
52
+ def get_children(self) -> list[martist.Artist]: ...
53
+ def get_bbox(self, renderer: RendererBase) -> Bbox: ...
54
+ def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ...
55
+
56
+ class PackerBase(OffsetBox):
57
+ height: float | None
58
+ width: float | None
59
+ sep: float | None
60
+ pad: float | None
61
+ mode: Literal["fixed", "expand", "equal"]
62
+ align: Literal["top", "bottom", "left", "right", "center", "baseline"]
63
+ def __init__(
64
+ self,
65
+ pad: float | None = ...,
66
+ sep: float | None = ...,
67
+ width: float | None = ...,
68
+ height: float | None = ...,
69
+ align: Literal["top", "bottom", "left", "right", "center", "baseline"] = ...,
70
+ mode: Literal["fixed", "expand", "equal"] = ...,
71
+ children: list[martist.Artist] | None = ...,
72
+ ) -> None: ...
73
+
74
+ class VPacker(PackerBase): ...
75
+ class HPacker(PackerBase): ...
76
+
77
+ class PaddedBox(OffsetBox):
78
+ pad: float | None
79
+ patch: FancyBboxPatch
80
+ def __init__(
81
+ self,
82
+ child: martist.Artist,
83
+ pad: float | None = ...,
84
+ *,
85
+ draw_frame: bool = ...,
86
+ patch_attrs: dict[str, Any] | None = ...,
87
+ ) -> None: ...
88
+ def update_frame(self, bbox: Bbox, fontsize: float | None = ...) -> None: ...
89
+ def draw_frame(self, renderer: RendererBase) -> None: ...
90
+
91
+ class DrawingArea(OffsetBox):
92
+ width: float
93
+ height: float
94
+ xdescent: float
95
+ ydescent: float
96
+ offset_transform: Transform
97
+ dpi_transform: Transform
98
+ def __init__(
99
+ self,
100
+ width: float,
101
+ height: float,
102
+ xdescent: float = ...,
103
+ ydescent: float = ...,
104
+ clip: bool = ...,
105
+ ) -> None: ...
106
+ @property
107
+ def clip_children(self) -> bool: ...
108
+ @clip_children.setter
109
+ def clip_children(self, val: bool) -> None: ...
110
+ def get_transform(self) -> Transform: ...
111
+
112
+ # does not accept all options of superclass
113
+ def set_offset(self, xy: tuple[float, float]) -> None: ... # type: ignore[override]
114
+ def get_offset(self) -> tuple[float, float]: ... # type: ignore[override]
115
+ def add_artist(self, a: martist.Artist) -> None: ...
116
+
117
+ class TextArea(OffsetBox):
118
+ offset_transform: Transform
119
+ def __init__(
120
+ self,
121
+ s: str,
122
+ *,
123
+ textprops: dict[str, Any] | None = ...,
124
+ multilinebaseline: bool = ...,
125
+ ) -> None: ...
126
+ def set_text(self, s: str) -> None: ...
127
+ def get_text(self) -> str: ...
128
+ def set_multilinebaseline(self, t: bool) -> None: ...
129
+ def get_multilinebaseline(self) -> bool: ...
130
+
131
+ # does not accept all options of superclass
132
+ def set_offset(self, xy: tuple[float, float]) -> None: ... # type: ignore[override]
133
+ def get_offset(self) -> tuple[float, float]: ... # type: ignore[override]
134
+
135
+ class AuxTransformBox(OffsetBox):
136
+ aux_transform: Transform
137
+ offset_transform: Transform
138
+ ref_offset_transform: Transform
139
+ def __init__(self, aux_transform: Transform) -> None: ...
140
+ def add_artist(self, a: martist.Artist) -> None: ...
141
+ def get_transform(self) -> Transform: ...
142
+
143
+ # does not accept all options of superclass
144
+ def set_offset(self, xy: tuple[float, float]) -> None: ... # type: ignore[override]
145
+ def get_offset(self) -> tuple[float, float]: ... # type: ignore[override]
146
+
147
+ class AnchoredOffsetbox(OffsetBox):
148
+ zorder: float
149
+ codes: dict[str, int]
150
+ loc: int
151
+ borderpad: float
152
+ pad: float
153
+ prop: FontProperties
154
+ patch: FancyBboxPatch
155
+ def __init__(
156
+ self,
157
+ loc: str,
158
+ *,
159
+ pad: float = ...,
160
+ borderpad: float = ...,
161
+ child: OffsetBox | None = ...,
162
+ prop: FontProperties | None = ...,
163
+ frameon: bool = ...,
164
+ bbox_to_anchor: BboxBase
165
+ | tuple[float, float]
166
+ | tuple[float, float, float, float]
167
+ | None = ...,
168
+ bbox_transform: Transform | None = ...,
169
+ **kwargs
170
+ ) -> None: ...
171
+ def set_child(self, child: OffsetBox | None) -> None: ...
172
+ def get_child(self) -> OffsetBox | None: ...
173
+ def get_children(self) -> list[martist.Artist]: ...
174
+ def get_bbox_to_anchor(self) -> Bbox: ...
175
+ def set_bbox_to_anchor(
176
+ self, bbox: BboxBase, transform: Transform | None = ...
177
+ ) -> None: ...
178
+ def update_frame(self, bbox: Bbox, fontsize: float | None = ...) -> None: ...
179
+
180
+ class AnchoredText(AnchoredOffsetbox):
181
+ txt: TextArea
182
+ def __init__(
183
+ self,
184
+ s: str,
185
+ loc: str,
186
+ *,
187
+ pad: float = ...,
188
+ borderpad: float = ...,
189
+ prop: dict[str, Any] | None = ...,
190
+ **kwargs
191
+ ) -> None: ...
192
+
193
+ class OffsetImage(OffsetBox):
194
+ image: BboxImage
195
+ def __init__(
196
+ self,
197
+ arr: ArrayLike,
198
+ *,
199
+ zoom: float = ...,
200
+ cmap: Colormap | str | None = ...,
201
+ norm: Normalize | str | None = ...,
202
+ interpolation: str | None = ...,
203
+ origin: Literal["upper", "lower"] | None = ...,
204
+ filternorm: bool = ...,
205
+ filterrad: float = ...,
206
+ resample: bool = ...,
207
+ dpi_cor: bool = ...,
208
+ **kwargs
209
+ ) -> None: ...
210
+ stale: bool
211
+ def set_data(self, arr: ArrayLike | None) -> None: ...
212
+ def get_data(self) -> ArrayLike | None: ...
213
+ def set_zoom(self, zoom: float) -> None: ...
214
+ def get_zoom(self) -> float: ...
215
+ def get_children(self) -> list[martist.Artist]: ...
216
+ def get_offset(self) -> tuple[float, float]: ... # type: ignore[override]
217
+
218
+ class AnnotationBbox(martist.Artist, mtext._AnnotationBase):
219
+ zorder: float
220
+ offsetbox: OffsetBox
221
+ arrowprops: dict[str, Any] | None
222
+ xybox: tuple[float, float]
223
+ boxcoords: CoordsType
224
+ arrow_patch: FancyArrowPatch | None
225
+ patch: FancyBboxPatch
226
+ prop: FontProperties
227
+ def __init__(
228
+ self,
229
+ offsetbox: OffsetBox,
230
+ xy: tuple[float, float],
231
+ xybox: tuple[float, float] | None = ...,
232
+ xycoords: CoordsType = ...,
233
+ boxcoords: CoordsType | None = ...,
234
+ *,
235
+ frameon: bool = ...,
236
+ pad: float = ...,
237
+ annotation_clip: bool | None = ...,
238
+ box_alignment: tuple[float, float] = ...,
239
+ bboxprops: dict[str, Any] | None = ...,
240
+ arrowprops: dict[str, Any] | None = ...,
241
+ fontsize: float | str | None = ...,
242
+ **kwargs
243
+ ) -> None: ...
244
+ @property
245
+ def xyann(self) -> tuple[float, float]: ...
246
+ @xyann.setter
247
+ def xyann(self, xyann: tuple[float, float]) -> None: ...
248
+ @property
249
+ def anncoords(
250
+ self,
251
+ ) -> CoordsType: ...
252
+ @anncoords.setter
253
+ def anncoords(
254
+ self,
255
+ coords: CoordsType,
256
+ ) -> None: ...
257
+ def get_children(self) -> list[martist.Artist]: ...
258
+ def set_figure(self, fig: Figure | SubFigure) -> None: ...
259
+ def set_fontsize(self, s: str | float | None = ...) -> None: ...
260
+ def get_fontsize(self) -> float: ...
261
+ def get_tightbbox(self, renderer: RendererBase | None = ...) -> Bbox: ...
262
+ def update_positions(self, renderer: RendererBase) -> None: ...
263
+
264
+ class DraggableBase:
265
+ ref_artist: martist.Artist
266
+ got_artist: bool
267
+ mouse_x: int
268
+ mouse_y: int
269
+ background: Any
270
+
271
+ @property
272
+ def canvas(self) -> FigureCanvasBase: ...
273
+ @property
274
+ def cids(self) -> list[int]: ...
275
+
276
+ def __init__(self, ref_artist: martist.Artist, use_blit: bool = ...) -> None: ...
277
+ def on_motion(self, evt: Event) -> None: ...
278
+ def on_pick(self, evt: Event) -> None: ...
279
+ def on_release(self, event: Event) -> None: ...
280
+ def disconnect(self) -> None: ...
281
+ def save_offset(self) -> None: ...
282
+ def update_offset(self, dx: float, dy: float) -> None: ...
283
+ def finalize_offset(self) -> None: ...
284
+
285
+ class DraggableOffsetBox(DraggableBase):
286
+ offsetbox: OffsetBox
287
+ def __init__(
288
+ self, ref_artist: martist.Artist, offsetbox: OffsetBox, use_blit: bool = ...
289
+ ) -> None: ...
290
+ def save_offset(self) -> None: ...
291
+ def update_offset(self, dx: float, dy: float) -> None: ...
292
+ def get_loc_in_canvas(self) -> tuple[float, float]: ...
293
+
294
+ class DraggableAnnotation(DraggableBase):
295
+ annotation: mtext.Annotation
296
+ def __init__(self, annotation: mtext.Annotation, use_blit: bool = ...) -> None: ...
297
+ def save_offset(self) -> None: ...
298
+ def update_offset(self, dx: float, dy: float) -> None: ...
lib/python3.12/site-packages/matplotlib/pyplot.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/matplotlib/sankey.pyi ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from matplotlib.axes import Axes
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from typing import Any
5
+ from typing_extensions import Self # < Py 3.11
6
+
7
+ import numpy as np
8
+
9
+ __license__: str
10
+ __credits__: list[str]
11
+ __author__: str
12
+ __version__: str
13
+
14
+ RIGHT: int
15
+ UP: int
16
+ DOWN: int
17
+
18
+ # TODO typing units
19
+ class Sankey:
20
+ diagrams: list[Any]
21
+ ax: Axes
22
+ unit: Any
23
+ format: str | Callable[[float], str]
24
+ scale: float
25
+ gap: float
26
+ radius: float
27
+ shoulder: float
28
+ offset: float
29
+ margin: float
30
+ pitch: float
31
+ tolerance: float
32
+ extent: np.ndarray
33
+ def __init__(
34
+ self,
35
+ ax: Axes | None = ...,
36
+ scale: float = ...,
37
+ unit: Any = ...,
38
+ format: str | Callable[[float], str] = ...,
39
+ gap: float = ...,
40
+ radius: float = ...,
41
+ shoulder: float = ...,
42
+ offset: float = ...,
43
+ head_angle: float = ...,
44
+ margin: float = ...,
45
+ tolerance: float = ...,
46
+ **kwargs
47
+ ) -> None: ...
48
+ def add(
49
+ self,
50
+ patchlabel: str = ...,
51
+ flows: Iterable[float] | None = ...,
52
+ orientations: Iterable[int] | None = ...,
53
+ labels: str | Iterable[str | None] = ...,
54
+ trunklength: float = ...,
55
+ pathlengths: float | Iterable[float] = ...,
56
+ prior: int | None = ...,
57
+ connect: tuple[int, int] = ...,
58
+ rotation: float = ...,
59
+ **kwargs
60
+ ) -> Self: ...
61
+ def finish(self) -> list[Any]: ...
lib/python3.12/site-packages/matplotlib/scale.py ADDED
@@ -0,0 +1,767 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Scales define the distribution of data values on an axis, e.g. a log scaling.
3
+
4
+ The mapping is implemented through `.Transform` subclasses.
5
+
6
+ The following scales are built-in:
7
+
8
+ .. _builtin_scales:
9
+
10
+ ============= ===================== ================================ =================================
11
+ Name Class Transform Inverted transform
12
+ ============= ===================== ================================ =================================
13
+ "asinh" `AsinhScale` `AsinhTransform` `InvertedAsinhTransform`
14
+ "function" `FuncScale` `FuncTransform` `FuncTransform`
15
+ "functionlog" `FuncScaleLog` `FuncTransform` + `LogTransform` `InvertedLogTransform` + `FuncTransform`
16
+ "linear" `LinearScale` `.IdentityTransform` `.IdentityTransform`
17
+ "log" `LogScale` `LogTransform` `InvertedLogTransform`
18
+ "logit" `LogitScale` `LogitTransform` `LogisticTransform`
19
+ "symlog" `SymmetricalLogScale` `SymmetricalLogTransform` `InvertedSymmetricalLogTransform`
20
+ ============= ===================== ================================ =================================
21
+
22
+ A user will often only use the scale name, e.g. when setting the scale through
23
+ `~.Axes.set_xscale`: ``ax.set_xscale("log")``.
24
+
25
+ See also the :ref:`scales examples <sphx_glr_gallery_scales>` in the documentation.
26
+
27
+ Custom scaling can be achieved through `FuncScale`, or by creating your own
28
+ `ScaleBase` subclass and corresponding transforms (see :doc:`/gallery/scales/custom_scale`).
29
+ Third parties can register their scales by name through `register_scale`.
30
+ """ # noqa: E501
31
+
32
+ import inspect
33
+ import textwrap
34
+
35
+ import numpy as np
36
+
37
+ import matplotlib as mpl
38
+ from matplotlib import _api, _docstring
39
+ from matplotlib.ticker import (
40
+ NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
41
+ NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
42
+ SymmetricalLogLocator, AsinhLocator, LogitLocator)
43
+ from matplotlib.transforms import Transform, IdentityTransform
44
+
45
+
46
+ class ScaleBase:
47
+ """
48
+ The base class for all scales.
49
+
50
+ Scales are separable transformations, working on a single dimension.
51
+
52
+ Subclasses should override
53
+
54
+ :attr:`name`
55
+ The scale's name.
56
+ :meth:`get_transform`
57
+ A method returning a `.Transform`, which converts data coordinates to
58
+ scaled coordinates. This transform should be invertible, so that e.g.
59
+ mouse positions can be converted back to data coordinates.
60
+ :meth:`set_default_locators_and_formatters`
61
+ A method that sets default locators and formatters for an `~.axis.Axis`
62
+ that uses this scale.
63
+ :meth:`limit_range_for_scale`
64
+ An optional method that "fixes" the axis range to acceptable values,
65
+ e.g. restricting log-scaled axes to positive values.
66
+ """
67
+
68
+ def __init__(self, axis):
69
+ r"""
70
+ Construct a new scale.
71
+
72
+ Notes
73
+ -----
74
+ The following note is for scale implementers.
75
+
76
+ For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
77
+ object as first argument. However, this argument should not
78
+ be used: a single scale object should be usable by multiple
79
+ `~matplotlib.axis.Axis`\es at the same time.
80
+ """
81
+
82
+ def get_transform(self):
83
+ """
84
+ Return the `.Transform` object associated with this scale.
85
+ """
86
+ raise NotImplementedError()
87
+
88
+ def set_default_locators_and_formatters(self, axis):
89
+ """
90
+ Set the locators and formatters of *axis* to instances suitable for
91
+ this scale.
92
+ """
93
+ raise NotImplementedError()
94
+
95
+ def limit_range_for_scale(self, vmin, vmax, minpos):
96
+ """
97
+ Return the range *vmin*, *vmax*, restricted to the
98
+ domain supported by this scale (if any).
99
+
100
+ *minpos* should be the minimum positive value in the data.
101
+ This is used by log scales to determine a minimum value.
102
+ """
103
+ return vmin, vmax
104
+
105
+
106
+ class LinearScale(ScaleBase):
107
+ """
108
+ The default linear scale.
109
+ """
110
+
111
+ name = 'linear'
112
+
113
+ def __init__(self, axis):
114
+ # This method is present only to prevent inheritance of the base class'
115
+ # constructor docstring, which would otherwise end up interpolated into
116
+ # the docstring of Axis.set_scale.
117
+ """
118
+ """ # noqa: D419
119
+
120
+ def set_default_locators_and_formatters(self, axis):
121
+ # docstring inherited
122
+ axis.set_major_locator(AutoLocator())
123
+ axis.set_major_formatter(ScalarFormatter())
124
+ axis.set_minor_formatter(NullFormatter())
125
+ # update the minor locator for x and y axis based on rcParams
126
+ if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
127
+ axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
128
+ axis.set_minor_locator(AutoMinorLocator())
129
+ else:
130
+ axis.set_minor_locator(NullLocator())
131
+
132
+ def get_transform(self):
133
+ """
134
+ Return the transform for linear scaling, which is just the
135
+ `~matplotlib.transforms.IdentityTransform`.
136
+ """
137
+ return IdentityTransform()
138
+
139
+
140
+ class FuncTransform(Transform):
141
+ """
142
+ A simple transform that takes and arbitrary function for the
143
+ forward and inverse transform.
144
+ """
145
+
146
+ input_dims = output_dims = 1
147
+
148
+ def __init__(self, forward, inverse):
149
+ """
150
+ Parameters
151
+ ----------
152
+ forward : callable
153
+ The forward function for the transform. This function must have
154
+ an inverse and, for best behavior, be monotonic.
155
+ It must have the signature::
156
+
157
+ def forward(values: array-like) -> array-like
158
+
159
+ inverse : callable
160
+ The inverse of the forward function. Signature as ``forward``.
161
+ """
162
+ super().__init__()
163
+ if callable(forward) and callable(inverse):
164
+ self._forward = forward
165
+ self._inverse = inverse
166
+ else:
167
+ raise ValueError('arguments to FuncTransform must be functions')
168
+
169
+ def transform_non_affine(self, values):
170
+ return self._forward(values)
171
+
172
+ def inverted(self):
173
+ return FuncTransform(self._inverse, self._forward)
174
+
175
+
176
+ class FuncScale(ScaleBase):
177
+ """
178
+ Provide an arbitrary scale with user-supplied function for the axis.
179
+ """
180
+
181
+ name = 'function'
182
+
183
+ def __init__(self, axis, functions):
184
+ """
185
+ Parameters
186
+ ----------
187
+ axis : `~matplotlib.axis.Axis`
188
+ The axis for the scale.
189
+ functions : (callable, callable)
190
+ two-tuple of the forward and inverse functions for the scale.
191
+ The forward function must be monotonic.
192
+
193
+ Both functions must have the signature::
194
+
195
+ def forward(values: array-like) -> array-like
196
+ """
197
+ forward, inverse = functions
198
+ transform = FuncTransform(forward, inverse)
199
+ self._transform = transform
200
+
201
+ def get_transform(self):
202
+ """Return the `.FuncTransform` associated with this scale."""
203
+ return self._transform
204
+
205
+ def set_default_locators_and_formatters(self, axis):
206
+ # docstring inherited
207
+ axis.set_major_locator(AutoLocator())
208
+ axis.set_major_formatter(ScalarFormatter())
209
+ axis.set_minor_formatter(NullFormatter())
210
+ # update the minor locator for x and y axis based on rcParams
211
+ if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
212
+ axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
213
+ axis.set_minor_locator(AutoMinorLocator())
214
+ else:
215
+ axis.set_minor_locator(NullLocator())
216
+
217
+
218
+ class LogTransform(Transform):
219
+ input_dims = output_dims = 1
220
+
221
+ def __init__(self, base, nonpositive='clip'):
222
+ super().__init__()
223
+ if base <= 0 or base == 1:
224
+ raise ValueError('The log base cannot be <= 0 or == 1')
225
+ self.base = base
226
+ self._clip = _api.check_getitem(
227
+ {"clip": True, "mask": False}, nonpositive=nonpositive)
228
+
229
+ def __str__(self):
230
+ return "{}(base={}, nonpositive={!r})".format(
231
+ type(self).__name__, self.base, "clip" if self._clip else "mask")
232
+
233
+ def transform_non_affine(self, values):
234
+ # Ignore invalid values due to nans being passed to the transform.
235
+ with np.errstate(divide="ignore", invalid="ignore"):
236
+ log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
237
+ if log: # If possible, do everything in a single call to NumPy.
238
+ out = log(values)
239
+ else:
240
+ out = np.log(values)
241
+ out /= np.log(self.base)
242
+ if self._clip:
243
+ # SVG spec says that conforming viewers must support values up
244
+ # to 3.4e38 (C float); however experiments suggest that
245
+ # Inkscape (which uses cairo for rendering) runs into cairo's
246
+ # 24-bit limit (which is apparently shared by Agg).
247
+ # Ghostscript (used for pdf rendering appears to overflow even
248
+ # earlier, with the max value around 2 ** 15 for the tests to
249
+ # pass. On the other hand, in practice, we want to clip beyond
250
+ # np.log10(np.nextafter(0, 1)) ~ -323
251
+ # so 1000 seems safe.
252
+ out[values <= 0] = -1000
253
+ return out
254
+
255
+ def inverted(self):
256
+ return InvertedLogTransform(self.base)
257
+
258
+
259
+ class InvertedLogTransform(Transform):
260
+ input_dims = output_dims = 1
261
+
262
+ def __init__(self, base):
263
+ super().__init__()
264
+ self.base = base
265
+
266
+ def __str__(self):
267
+ return f"{type(self).__name__}(base={self.base})"
268
+
269
+ def transform_non_affine(self, values):
270
+ return np.power(self.base, values)
271
+
272
+ def inverted(self):
273
+ return LogTransform(self.base)
274
+
275
+
276
+ class LogScale(ScaleBase):
277
+ """
278
+ A standard logarithmic scale. Care is taken to only plot positive values.
279
+ """
280
+ name = 'log'
281
+
282
+ def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"):
283
+ """
284
+ Parameters
285
+ ----------
286
+ axis : `~matplotlib.axis.Axis`
287
+ The axis for the scale.
288
+ base : float, default: 10
289
+ The base of the logarithm.
290
+ nonpositive : {'clip', 'mask'}, default: 'clip'
291
+ Determines the behavior for non-positive values. They can either
292
+ be masked as invalid, or clipped to a very small positive number.
293
+ subs : sequence of int, default: None
294
+ Where to place the subticks between each major tick. For example,
295
+ in a log10 scale, ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place 8
296
+ logarithmically spaced minor ticks between each major tick.
297
+ """
298
+ self._transform = LogTransform(base, nonpositive)
299
+ self.subs = subs
300
+
301
+ base = property(lambda self: self._transform.base)
302
+
303
+ def set_default_locators_and_formatters(self, axis):
304
+ # docstring inherited
305
+ axis.set_major_locator(LogLocator(self.base))
306
+ axis.set_major_formatter(LogFormatterSciNotation(self.base))
307
+ axis.set_minor_locator(LogLocator(self.base, self.subs))
308
+ axis.set_minor_formatter(
309
+ LogFormatterSciNotation(self.base,
310
+ labelOnlyBase=(self.subs is not None)))
311
+
312
+ def get_transform(self):
313
+ """Return the `.LogTransform` associated with this scale."""
314
+ return self._transform
315
+
316
+ def limit_range_for_scale(self, vmin, vmax, minpos):
317
+ """Limit the domain to positive values."""
318
+ if not np.isfinite(minpos):
319
+ minpos = 1e-300 # Should rarely (if ever) have a visible effect.
320
+
321
+ return (minpos if vmin <= 0 else vmin,
322
+ minpos if vmax <= 0 else vmax)
323
+
324
+
325
+ class FuncScaleLog(LogScale):
326
+ """
327
+ Provide an arbitrary scale with user-supplied function for the axis and
328
+ then put on a logarithmic axes.
329
+ """
330
+
331
+ name = 'functionlog'
332
+
333
+ def __init__(self, axis, functions, base=10):
334
+ """
335
+ Parameters
336
+ ----------
337
+ axis : `~matplotlib.axis.Axis`
338
+ The axis for the scale.
339
+ functions : (callable, callable)
340
+ two-tuple of the forward and inverse functions for the scale.
341
+ The forward function must be monotonic.
342
+
343
+ Both functions must have the signature::
344
+
345
+ def forward(values: array-like) -> array-like
346
+
347
+ base : float, default: 10
348
+ Logarithmic base of the scale.
349
+ """
350
+ forward, inverse = functions
351
+ self.subs = None
352
+ self._transform = FuncTransform(forward, inverse) + LogTransform(base)
353
+
354
+ @property
355
+ def base(self):
356
+ return self._transform._b.base # Base of the LogTransform.
357
+
358
+ def get_transform(self):
359
+ """Return the `.Transform` associated with this scale."""
360
+ return self._transform
361
+
362
+
363
+ class SymmetricalLogTransform(Transform):
364
+ input_dims = output_dims = 1
365
+
366
+ def __init__(self, base, linthresh, linscale):
367
+ super().__init__()
368
+ if base <= 1.0:
369
+ raise ValueError("'base' must be larger than 1")
370
+ if linthresh <= 0.0:
371
+ raise ValueError("'linthresh' must be positive")
372
+ if linscale <= 0.0:
373
+ raise ValueError("'linscale' must be positive")
374
+ self.base = base
375
+ self.linthresh = linthresh
376
+ self.linscale = linscale
377
+ self._linscale_adj = (linscale / (1.0 - self.base ** -1))
378
+ self._log_base = np.log(base)
379
+
380
+ def transform_non_affine(self, values):
381
+ abs_a = np.abs(values)
382
+ with np.errstate(divide="ignore", invalid="ignore"):
383
+ out = np.sign(values) * self.linthresh * (
384
+ self._linscale_adj +
385
+ np.log(abs_a / self.linthresh) / self._log_base)
386
+ inside = abs_a <= self.linthresh
387
+ out[inside] = values[inside] * self._linscale_adj
388
+ return out
389
+
390
+ def inverted(self):
391
+ return InvertedSymmetricalLogTransform(self.base, self.linthresh,
392
+ self.linscale)
393
+
394
+
395
+ class InvertedSymmetricalLogTransform(Transform):
396
+ input_dims = output_dims = 1
397
+
398
+ def __init__(self, base, linthresh, linscale):
399
+ super().__init__()
400
+ symlog = SymmetricalLogTransform(base, linthresh, linscale)
401
+ self.base = base
402
+ self.linthresh = linthresh
403
+ self.invlinthresh = symlog.transform(linthresh)
404
+ self.linscale = linscale
405
+ self._linscale_adj = (linscale / (1.0 - self.base ** -1))
406
+
407
+ def transform_non_affine(self, values):
408
+ abs_a = np.abs(values)
409
+ with np.errstate(divide="ignore", invalid="ignore"):
410
+ out = np.sign(values) * self.linthresh * (
411
+ np.power(self.base,
412
+ abs_a / self.linthresh - self._linscale_adj))
413
+ inside = abs_a <= self.invlinthresh
414
+ out[inside] = values[inside] / self._linscale_adj
415
+ return out
416
+
417
+ def inverted(self):
418
+ return SymmetricalLogTransform(self.base,
419
+ self.linthresh, self.linscale)
420
+
421
+
422
+ class SymmetricalLogScale(ScaleBase):
423
+ """
424
+ The symmetrical logarithmic scale is logarithmic in both the
425
+ positive and negative directions from the origin.
426
+
427
+ Since the values close to zero tend toward infinity, there is a
428
+ need to have a range around zero that is linear. The parameter
429
+ *linthresh* allows the user to specify the size of this range
430
+ (-*linthresh*, *linthresh*).
431
+
432
+ See :doc:`/gallery/scales/symlog_demo` for a detailed description.
433
+
434
+ Parameters
435
+ ----------
436
+ base : float, default: 10
437
+ The base of the logarithm.
438
+
439
+ linthresh : float, default: 2
440
+ Defines the range ``(-x, x)``, within which the plot is linear.
441
+ This avoids having the plot go to infinity around zero.
442
+
443
+ subs : sequence of int
444
+ Where to place the subticks between each major tick.
445
+ For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
446
+ 8 logarithmically spaced minor ticks between each major tick.
447
+
448
+ linscale : float, optional
449
+ This allows the linear range ``(-linthresh, linthresh)`` to be
450
+ stretched relative to the logarithmic range. Its value is the number of
451
+ decades to use for each half of the linear range. For example, when
452
+ *linscale* == 1.0 (the default), the space used for the positive and
453
+ negative halves of the linear range will be equal to one decade in
454
+ the logarithmic range.
455
+ """
456
+ name = 'symlog'
457
+
458
+ def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1):
459
+ self._transform = SymmetricalLogTransform(base, linthresh, linscale)
460
+ self.subs = subs
461
+
462
+ base = property(lambda self: self._transform.base)
463
+ linthresh = property(lambda self: self._transform.linthresh)
464
+ linscale = property(lambda self: self._transform.linscale)
465
+
466
+ def set_default_locators_and_formatters(self, axis):
467
+ # docstring inherited
468
+ axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
469
+ axis.set_major_formatter(LogFormatterSciNotation(self.base))
470
+ axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
471
+ self.subs))
472
+ axis.set_minor_formatter(NullFormatter())
473
+
474
+ def get_transform(self):
475
+ """Return the `.SymmetricalLogTransform` associated with this scale."""
476
+ return self._transform
477
+
478
+
479
+ class AsinhTransform(Transform):
480
+ """Inverse hyperbolic-sine transformation used by `.AsinhScale`"""
481
+ input_dims = output_dims = 1
482
+
483
+ def __init__(self, linear_width):
484
+ super().__init__()
485
+ if linear_width <= 0.0:
486
+ raise ValueError("Scale parameter 'linear_width' " +
487
+ "must be strictly positive")
488
+ self.linear_width = linear_width
489
+
490
+ def transform_non_affine(self, values):
491
+ return self.linear_width * np.arcsinh(values / self.linear_width)
492
+
493
+ def inverted(self):
494
+ return InvertedAsinhTransform(self.linear_width)
495
+
496
+
497
+ class InvertedAsinhTransform(Transform):
498
+ """Hyperbolic sine transformation used by `.AsinhScale`"""
499
+ input_dims = output_dims = 1
500
+
501
+ def __init__(self, linear_width):
502
+ super().__init__()
503
+ self.linear_width = linear_width
504
+
505
+ def transform_non_affine(self, values):
506
+ return self.linear_width * np.sinh(values / self.linear_width)
507
+
508
+ def inverted(self):
509
+ return AsinhTransform(self.linear_width)
510
+
511
+
512
+ class AsinhScale(ScaleBase):
513
+ """
514
+ A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh)
515
+
516
+ For values close to zero, this is essentially a linear scale,
517
+ but for large magnitude values (either positive or negative)
518
+ it is asymptotically logarithmic. The transition between these
519
+ linear and logarithmic regimes is smooth, and has no discontinuities
520
+ in the function gradient in contrast to
521
+ the `.SymmetricalLogScale` ("symlog") scale.
522
+
523
+ Specifically, the transformation of an axis coordinate :math:`a` is
524
+ :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0`
525
+ is the effective width of the linear region of the transformation.
526
+ In that region, the transformation is
527
+ :math:`a \\rightarrow a + \\mathcal{O}(a^3)`.
528
+ For large values of :math:`a` the transformation behaves as
529
+ :math:`a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1)`.
530
+
531
+ .. note::
532
+
533
+ This API is provisional and may be revised in the future
534
+ based on early user feedback.
535
+ """
536
+
537
+ name = 'asinh'
538
+
539
+ auto_tick_multipliers = {
540
+ 3: (2, ),
541
+ 4: (2, ),
542
+ 5: (2, ),
543
+ 8: (2, 4),
544
+ 10: (2, 5),
545
+ 16: (2, 4, 8),
546
+ 64: (4, 16),
547
+ 1024: (256, 512)
548
+ }
549
+
550
+ def __init__(self, axis, *, linear_width=1.0,
551
+ base=10, subs='auto', **kwargs):
552
+ """
553
+ Parameters
554
+ ----------
555
+ linear_width : float, default: 1
556
+ The scale parameter (elsewhere referred to as :math:`a_0`)
557
+ defining the extent of the quasi-linear region,
558
+ and the coordinate values beyond which the transformation
559
+ becomes asymptotically logarithmic.
560
+ base : int, default: 10
561
+ The number base used for rounding tick locations
562
+ on a logarithmic scale. If this is less than one,
563
+ then rounding is to the nearest integer multiple
564
+ of powers of ten.
565
+ subs : sequence of int
566
+ Multiples of the number base used for minor ticks.
567
+ If set to 'auto', this will use built-in defaults,
568
+ e.g. (2, 5) for base=10.
569
+ """
570
+ super().__init__(axis)
571
+ self._transform = AsinhTransform(linear_width)
572
+ self._base = int(base)
573
+ if subs == 'auto':
574
+ self._subs = self.auto_tick_multipliers.get(self._base)
575
+ else:
576
+ self._subs = subs
577
+
578
+ linear_width = property(lambda self: self._transform.linear_width)
579
+
580
+ def get_transform(self):
581
+ return self._transform
582
+
583
+ def set_default_locators_and_formatters(self, axis):
584
+ axis.set(major_locator=AsinhLocator(self.linear_width,
585
+ base=self._base),
586
+ minor_locator=AsinhLocator(self.linear_width,
587
+ base=self._base,
588
+ subs=self._subs),
589
+ minor_formatter=NullFormatter())
590
+ if self._base > 1:
591
+ axis.set_major_formatter(LogFormatterSciNotation(self._base))
592
+ else:
593
+ axis.set_major_formatter('{x:.3g}')
594
+
595
+
596
+ class LogitTransform(Transform):
597
+ input_dims = output_dims = 1
598
+
599
+ def __init__(self, nonpositive='mask'):
600
+ super().__init__()
601
+ _api.check_in_list(['mask', 'clip'], nonpositive=nonpositive)
602
+ self._nonpositive = nonpositive
603
+ self._clip = {"clip": True, "mask": False}[nonpositive]
604
+
605
+ def transform_non_affine(self, values):
606
+ """logit transform (base 10), masked or clipped"""
607
+ with np.errstate(divide="ignore", invalid="ignore"):
608
+ out = np.log10(values / (1 - values))
609
+ if self._clip: # See LogTransform for choice of clip value.
610
+ out[values <= 0] = -1000
611
+ out[1 <= values] = 1000
612
+ return out
613
+
614
+ def inverted(self):
615
+ return LogisticTransform(self._nonpositive)
616
+
617
+ def __str__(self):
618
+ return f"{type(self).__name__}({self._nonpositive!r})"
619
+
620
+
621
+ class LogisticTransform(Transform):
622
+ input_dims = output_dims = 1
623
+
624
+ def __init__(self, nonpositive='mask'):
625
+ super().__init__()
626
+ self._nonpositive = nonpositive
627
+
628
+ def transform_non_affine(self, values):
629
+ """logistic transform (base 10)"""
630
+ return 1.0 / (1 + 10**(-values))
631
+
632
+ def inverted(self):
633
+ return LogitTransform(self._nonpositive)
634
+
635
+ def __str__(self):
636
+ return f"{type(self).__name__}({self._nonpositive!r})"
637
+
638
+
639
+ class LogitScale(ScaleBase):
640
+ """
641
+ Logit scale for data between zero and one, both excluded.
642
+
643
+ This scale is similar to a log scale close to zero and to one, and almost
644
+ linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
645
+ """
646
+ name = 'logit'
647
+
648
+ def __init__(self, axis, nonpositive='mask', *,
649
+ one_half=r"\frac{1}{2}", use_overline=False):
650
+ r"""
651
+ Parameters
652
+ ----------
653
+ axis : `~matplotlib.axis.Axis`
654
+ Currently unused.
655
+ nonpositive : {'mask', 'clip'}
656
+ Determines the behavior for values beyond the open interval ]0, 1[.
657
+ They can either be masked as invalid, or clipped to a number very
658
+ close to 0 or 1.
659
+ use_overline : bool, default: False
660
+ Indicate the usage of survival notation (\overline{x}) in place of
661
+ standard notation (1-x) for probability close to one.
662
+ one_half : str, default: r"\frac{1}{2}"
663
+ The string used for ticks formatter to represent 1/2.
664
+ """
665
+ self._transform = LogitTransform(nonpositive)
666
+ self._use_overline = use_overline
667
+ self._one_half = one_half
668
+
669
+ def get_transform(self):
670
+ """Return the `.LogitTransform` associated with this scale."""
671
+ return self._transform
672
+
673
+ def set_default_locators_and_formatters(self, axis):
674
+ # docstring inherited
675
+ # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
676
+ axis.set_major_locator(LogitLocator())
677
+ axis.set_major_formatter(
678
+ LogitFormatter(
679
+ one_half=self._one_half,
680
+ use_overline=self._use_overline
681
+ )
682
+ )
683
+ axis.set_minor_locator(LogitLocator(minor=True))
684
+ axis.set_minor_formatter(
685
+ LogitFormatter(
686
+ minor=True,
687
+ one_half=self._one_half,
688
+ use_overline=self._use_overline
689
+ )
690
+ )
691
+
692
+ def limit_range_for_scale(self, vmin, vmax, minpos):
693
+ """
694
+ Limit the domain to values between 0 and 1 (excluded).
695
+ """
696
+ if not np.isfinite(minpos):
697
+ minpos = 1e-7 # Should rarely (if ever) have a visible effect.
698
+ return (minpos if vmin <= 0 else vmin,
699
+ 1 - minpos if vmax >= 1 else vmax)
700
+
701
+
702
+ _scale_mapping = {
703
+ 'linear': LinearScale,
704
+ 'log': LogScale,
705
+ 'symlog': SymmetricalLogScale,
706
+ 'asinh': AsinhScale,
707
+ 'logit': LogitScale,
708
+ 'function': FuncScale,
709
+ 'functionlog': FuncScaleLog,
710
+ }
711
+
712
+
713
+ def get_scale_names():
714
+ """Return the names of the available scales."""
715
+ return sorted(_scale_mapping)
716
+
717
+
718
+ def scale_factory(scale, axis, **kwargs):
719
+ """
720
+ Return a scale class by name.
721
+
722
+ Parameters
723
+ ----------
724
+ scale : {%(names)s}
725
+ axis : `~matplotlib.axis.Axis`
726
+ """
727
+ scale_cls = _api.check_getitem(_scale_mapping, scale=scale)
728
+ return scale_cls(axis, **kwargs)
729
+
730
+
731
+ if scale_factory.__doc__:
732
+ scale_factory.__doc__ = scale_factory.__doc__ % {
733
+ "names": ", ".join(map(repr, get_scale_names()))}
734
+
735
+
736
+ def register_scale(scale_class):
737
+ """
738
+ Register a new kind of scale.
739
+
740
+ Parameters
741
+ ----------
742
+ scale_class : subclass of `ScaleBase`
743
+ The scale to register.
744
+ """
745
+ _scale_mapping[scale_class.name] = scale_class
746
+
747
+
748
+ def _get_scale_docs():
749
+ """
750
+ Helper function for generating docstrings related to scales.
751
+ """
752
+ docs = []
753
+ for name, scale_class in _scale_mapping.items():
754
+ docstring = inspect.getdoc(scale_class.__init__) or ""
755
+ docs.extend([
756
+ f" {name!r}",
757
+ "",
758
+ textwrap.indent(docstring, " " * 8),
759
+ ""
760
+ ])
761
+ return "\n".join(docs)
762
+
763
+
764
+ _docstring.interpd.register(
765
+ scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]),
766
+ scale_docs=_get_scale_docs().rstrip(),
767
+ )
lib/python3.12/site-packages/matplotlib/texmanager.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""
2
+ Support for embedded TeX expressions in Matplotlib.
3
+
4
+ Requirements:
5
+
6
+ * LaTeX.
7
+ * \*Agg backends: dvipng>=1.6.
8
+ * PS backend: PSfrag, dvips, and Ghostscript>=9.0.
9
+ * PDF and SVG backends: if LuaTeX is present, it will be used to speed up some
10
+ post-processing steps, but note that it is not used to parse the TeX string
11
+ itself (only LaTeX is supported).
12
+
13
+ To enable TeX rendering of all text in your Matplotlib figure, set
14
+ :rc:`text.usetex` to True.
15
+
16
+ TeX and dvipng/dvips processing results are cached
17
+ in ~/.matplotlib/tex.cache for reuse between sessions.
18
+
19
+ `TexManager.get_rgba` can also be used to directly obtain raster output as RGBA
20
+ NumPy arrays.
21
+ """
22
+
23
+ import functools
24
+ import hashlib
25
+ import logging
26
+ import os
27
+ from pathlib import Path
28
+ import subprocess
29
+ from tempfile import TemporaryDirectory
30
+
31
+ import numpy as np
32
+
33
+ import matplotlib as mpl
34
+ from matplotlib import cbook, dviread
35
+
36
+ _log = logging.getLogger(__name__)
37
+
38
+
39
+ def _usepackage_if_not_loaded(package, *, option=None):
40
+ """
41
+ Output LaTeX code that loads a package (possibly with an option) if it
42
+ hasn't been loaded yet.
43
+
44
+ LaTeX cannot load twice a package with different options, so this helper
45
+ can be used to protect against users loading arbitrary packages/options in
46
+ their custom preamble.
47
+ """
48
+ option = f"[{option}]" if option is not None else ""
49
+ return (
50
+ r"\makeatletter"
51
+ r"\@ifpackageloaded{%(package)s}{}{\usepackage%(option)s{%(package)s}}"
52
+ r"\makeatother"
53
+ ) % {"package": package, "option": option}
54
+
55
+
56
+ class TexManager:
57
+ """
58
+ Convert strings to dvi files using TeX, caching the results to a directory.
59
+
60
+ The cache directory is called ``tex.cache`` and is located in the directory
61
+ returned by `.get_cachedir`.
62
+
63
+ Repeated calls to this constructor always return the same instance.
64
+ """
65
+
66
+ _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache')
67
+ _grey_arrayd = {}
68
+
69
+ _font_families = ('serif', 'sans-serif', 'cursive', 'monospace')
70
+ _font_preambles = {
71
+ 'new century schoolbook': r'\renewcommand{\rmdefault}{pnc}',
72
+ 'bookman': r'\renewcommand{\rmdefault}{pbk}',
73
+ 'times': r'\usepackage{mathptmx}',
74
+ 'palatino': r'\usepackage{mathpazo}',
75
+ 'zapf chancery': r'\usepackage{chancery}',
76
+ 'cursive': r'\usepackage{chancery}',
77
+ 'charter': r'\usepackage{charter}',
78
+ 'serif': '',
79
+ 'sans-serif': '',
80
+ 'helvetica': r'\usepackage{helvet}',
81
+ 'avant garde': r'\usepackage{avant}',
82
+ 'courier': r'\usepackage{courier}',
83
+ # Loading the type1ec package ensures that cm-super is installed, which
84
+ # is necessary for Unicode computer modern. (It also allows the use of
85
+ # computer modern at arbitrary sizes, but that's just a side effect.)
86
+ 'monospace': r'\usepackage{type1ec}',
87
+ 'computer modern roman': r'\usepackage{type1ec}',
88
+ 'computer modern sans serif': r'\usepackage{type1ec}',
89
+ 'computer modern typewriter': r'\usepackage{type1ec}',
90
+ }
91
+ _font_types = {
92
+ 'new century schoolbook': 'serif',
93
+ 'bookman': 'serif',
94
+ 'times': 'serif',
95
+ 'palatino': 'serif',
96
+ 'zapf chancery': 'cursive',
97
+ 'charter': 'serif',
98
+ 'helvetica': 'sans-serif',
99
+ 'avant garde': 'sans-serif',
100
+ 'courier': 'monospace',
101
+ 'computer modern roman': 'serif',
102
+ 'computer modern sans serif': 'sans-serif',
103
+ 'computer modern typewriter': 'monospace',
104
+ }
105
+
106
+ @functools.lru_cache # Always return the same instance.
107
+ def __new__(cls):
108
+ Path(cls._texcache).mkdir(parents=True, exist_ok=True)
109
+ return object.__new__(cls)
110
+
111
+ @classmethod
112
+ def _get_font_family_and_reduced(cls):
113
+ """Return the font family name and whether the font is reduced."""
114
+ ff = mpl.rcParams['font.family']
115
+ ff_val = ff[0].lower() if len(ff) == 1 else None
116
+ if len(ff) == 1 and ff_val in cls._font_families:
117
+ return ff_val, False
118
+ elif len(ff) == 1 and ff_val in cls._font_preambles:
119
+ return cls._font_types[ff_val], True
120
+ else:
121
+ _log.info('font.family must be one of (%s) when text.usetex is '
122
+ 'True. serif will be used by default.',
123
+ ', '.join(cls._font_families))
124
+ return 'serif', False
125
+
126
+ @classmethod
127
+ def _get_font_preamble_and_command(cls):
128
+ requested_family, is_reduced_font = cls._get_font_family_and_reduced()
129
+
130
+ preambles = {}
131
+ for font_family in cls._font_families:
132
+ if is_reduced_font and font_family == requested_family:
133
+ preambles[font_family] = cls._font_preambles[
134
+ mpl.rcParams['font.family'][0].lower()]
135
+ else:
136
+ rcfonts = mpl.rcParams[f"font.{font_family}"]
137
+ for i, font in enumerate(map(str.lower, rcfonts)):
138
+ if font in cls._font_preambles:
139
+ preambles[font_family] = cls._font_preambles[font]
140
+ _log.debug(
141
+ 'family: %s, package: %s, font: %s, skipped: %s',
142
+ font_family, cls._font_preambles[font], rcfonts[i],
143
+ ', '.join(rcfonts[:i]),
144
+ )
145
+ break
146
+ else:
147
+ _log.info('No LaTeX-compatible font found for the %s font'
148
+ 'family in rcParams. Using default.',
149
+ font_family)
150
+ preambles[font_family] = cls._font_preambles[font_family]
151
+
152
+ # The following packages and commands need to be included in the latex
153
+ # file's preamble:
154
+ cmd = {preambles[family]
155
+ for family in ['serif', 'sans-serif', 'monospace']}
156
+ if requested_family == 'cursive':
157
+ cmd.add(preambles['cursive'])
158
+ cmd.add(r'\usepackage{type1cm}')
159
+ preamble = '\n'.join(sorted(cmd))
160
+ fontcmd = (r'\sffamily' if requested_family == 'sans-serif' else
161
+ r'\ttfamily' if requested_family == 'monospace' else
162
+ r'\rmfamily')
163
+ return preamble, fontcmd
164
+
165
+ @classmethod
166
+ def get_basefile(cls, tex, fontsize, dpi=None):
167
+ """
168
+ Return a filename based on a hash of the string, fontsize, and dpi.
169
+ """
170
+ src = cls._get_tex_source(tex, fontsize) + str(dpi)
171
+ filehash = hashlib.sha256(
172
+ src.encode('utf-8'),
173
+ usedforsecurity=False
174
+ ).hexdigest()
175
+ filepath = Path(cls._texcache)
176
+
177
+ num_letters, num_levels = 2, 2
178
+ for i in range(0, num_letters*num_levels, num_letters):
179
+ filepath = filepath / Path(filehash[i:i+2])
180
+
181
+ filepath.mkdir(parents=True, exist_ok=True)
182
+ return os.path.join(filepath, filehash)
183
+
184
+ @classmethod
185
+ def get_font_preamble(cls):
186
+ """
187
+ Return a string containing font configuration for the tex preamble.
188
+ """
189
+ font_preamble, command = cls._get_font_preamble_and_command()
190
+ return font_preamble
191
+
192
+ @classmethod
193
+ def get_custom_preamble(cls):
194
+ """Return a string containing user additions to the tex preamble."""
195
+ return mpl.rcParams['text.latex.preamble']
196
+
197
+ @classmethod
198
+ def _get_tex_source(cls, tex, fontsize):
199
+ """Return the complete TeX source for processing a TeX string."""
200
+ font_preamble, fontcmd = cls._get_font_preamble_and_command()
201
+ baselineskip = 1.25 * fontsize
202
+ return "\n".join([
203
+ r"\documentclass{article}",
204
+ r"% Pass-through \mathdefault, which is used in non-usetex mode",
205
+ r"% to use the default text font but was historically suppressed",
206
+ r"% in usetex mode.",
207
+ r"\newcommand{\mathdefault}[1]{#1}",
208
+ font_preamble,
209
+ r"\usepackage[utf8]{inputenc}",
210
+ r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}",
211
+ r"% geometry is loaded before the custom preamble as ",
212
+ r"% convert_psfrags relies on a custom preamble to change the ",
213
+ r"% geometry.",
214
+ r"\usepackage[papersize=72in, margin=1in]{geometry}",
215
+ cls.get_custom_preamble(),
216
+ r"% Use `underscore` package to take care of underscores in text.",
217
+ r"% The [strings] option allows to use underscores in file names.",
218
+ _usepackage_if_not_loaded("underscore", option="strings"),
219
+ r"% Custom packages (e.g. newtxtext) may already have loaded ",
220
+ r"% textcomp with different options.",
221
+ _usepackage_if_not_loaded("textcomp"),
222
+ r"\pagestyle{empty}",
223
+ r"\begin{document}",
224
+ r"% The empty hbox ensures that a page is printed even for empty",
225
+ r"% inputs, except when using psfrag which gets confused by it.",
226
+ r"% matplotlibbaselinemarker is used by dviread to detect the",
227
+ r"% last line's baseline.",
228
+ rf"\fontsize{{{fontsize}}}{{{baselineskip}}}%",
229
+ r"\ifdefined\psfrag\else\hbox{}\fi%",
230
+ rf"{{{fontcmd} {tex}}}%",
231
+ r"\end{document}",
232
+ ])
233
+
234
+ @classmethod
235
+ def make_tex(cls, tex, fontsize):
236
+ """
237
+ Generate a tex file to render the tex string at a specific font size.
238
+
239
+ Return the file name.
240
+ """
241
+ texfile = cls.get_basefile(tex, fontsize) + ".tex"
242
+ Path(texfile).write_text(cls._get_tex_source(tex, fontsize),
243
+ encoding='utf-8')
244
+ return texfile
245
+
246
+ @classmethod
247
+ def _run_checked_subprocess(cls, command, tex, *, cwd=None):
248
+ _log.debug(cbook._pformat_subprocess(command))
249
+ try:
250
+ report = subprocess.check_output(
251
+ command, cwd=cwd if cwd is not None else cls._texcache,
252
+ stderr=subprocess.STDOUT)
253
+ except FileNotFoundError as exc:
254
+ raise RuntimeError(
255
+ f'Failed to process string with tex because {command[0]} '
256
+ 'could not be found') from exc
257
+ except subprocess.CalledProcessError as exc:
258
+ raise RuntimeError(
259
+ '{prog} was not able to process the following string:\n'
260
+ '{tex!r}\n\n'
261
+ 'Here is the full command invocation and its output:\n\n'
262
+ '{format_command}\n\n'
263
+ '{exc}\n\n'.format(
264
+ prog=command[0],
265
+ format_command=cbook._pformat_subprocess(command),
266
+ tex=tex.encode('unicode_escape'),
267
+ exc=exc.output.decode('utf-8', 'backslashreplace'))
268
+ ) from None
269
+ _log.debug(report)
270
+ return report
271
+
272
+ @classmethod
273
+ def make_dvi(cls, tex, fontsize):
274
+ """
275
+ Generate a dvi file containing latex's layout of tex string.
276
+
277
+ Return the file name.
278
+ """
279
+ basefile = cls.get_basefile(tex, fontsize)
280
+ dvifile = '%s.dvi' % basefile
281
+ if not os.path.exists(dvifile):
282
+ texfile = Path(cls.make_tex(tex, fontsize))
283
+ # Generate the dvi in a temporary directory to avoid race
284
+ # conditions e.g. if multiple processes try to process the same tex
285
+ # string at the same time. Having tmpdir be a subdirectory of the
286
+ # final output dir ensures that they are on the same filesystem,
287
+ # and thus replace() works atomically. It also allows referring to
288
+ # the texfile with a relative path (for pathological MPLCONFIGDIRs,
289
+ # the absolute path may contain characters (e.g. ~) that TeX does
290
+ # not support; n.b. relative paths cannot traverse parents, or it
291
+ # will be blocked when `openin_any = p` in texmf.cnf).
292
+ cwd = Path(dvifile).parent
293
+ with TemporaryDirectory(dir=cwd) as tmpdir:
294
+ tmppath = Path(tmpdir)
295
+ cls._run_checked_subprocess(
296
+ ["latex", "-interaction=nonstopmode", "--halt-on-error",
297
+ f"--output-directory={tmppath.name}",
298
+ f"{texfile.name}"], tex, cwd=cwd)
299
+ (tmppath / Path(dvifile).name).replace(dvifile)
300
+ return dvifile
301
+
302
+ @classmethod
303
+ def make_png(cls, tex, fontsize, dpi):
304
+ """
305
+ Generate a png file containing latex's rendering of tex string.
306
+
307
+ Return the file name.
308
+ """
309
+ basefile = cls.get_basefile(tex, fontsize, dpi)
310
+ pngfile = '%s.png' % basefile
311
+ # see get_rgba for a discussion of the background
312
+ if not os.path.exists(pngfile):
313
+ dvifile = cls.make_dvi(tex, fontsize)
314
+ cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
315
+ "-T", "tight", "-o", pngfile, dvifile]
316
+ # When testing, disable FreeType rendering for reproducibility; but
317
+ # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0
318
+ # mode, so for it we keep FreeType enabled; the image will be
319
+ # slightly off.
320
+ if (getattr(mpl, "_called_from_pytest", False) and
321
+ mpl._get_executable_info("dvipng").raw_version != "1.16"):
322
+ cmd.insert(1, "--freetype0")
323
+ cls._run_checked_subprocess(cmd, tex)
324
+ return pngfile
325
+
326
+ @classmethod
327
+ def get_grey(cls, tex, fontsize=None, dpi=None):
328
+ """Return the alpha channel."""
329
+ if not fontsize:
330
+ fontsize = mpl.rcParams['font.size']
331
+ if not dpi:
332
+ dpi = mpl.rcParams['savefig.dpi']
333
+ key = cls._get_tex_source(tex, fontsize), dpi
334
+ alpha = cls._grey_arrayd.get(key)
335
+ if alpha is None:
336
+ pngfile = cls.make_png(tex, fontsize, dpi)
337
+ rgba = mpl.image.imread(os.path.join(cls._texcache, pngfile))
338
+ cls._grey_arrayd[key] = alpha = rgba[:, :, -1]
339
+ return alpha
340
+
341
+ @classmethod
342
+ def get_rgba(cls, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)):
343
+ r"""
344
+ Return latex's rendering of the tex string as an RGBA array.
345
+
346
+ Examples
347
+ --------
348
+ >>> texmanager = TexManager()
349
+ >>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!"
350
+ >>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0))
351
+ """
352
+ alpha = cls.get_grey(tex, fontsize, dpi)
353
+ rgba = np.empty((*alpha.shape, 4))
354
+ rgba[..., :3] = mpl.colors.to_rgb(rgb)
355
+ rgba[..., -1] = alpha
356
+ return rgba
357
+
358
+ @classmethod
359
+ def get_text_width_height_descent(cls, tex, fontsize, renderer=None):
360
+ """Return width, height and descent of the text."""
361
+ if tex.strip() == '':
362
+ return 0, 0, 0
363
+ dvifile = cls.make_dvi(tex, fontsize)
364
+ dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
365
+ with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
366
+ page, = dvi
367
+ # A total height (including the descent) needs to be returned.
368
+ return page.width, page.height + page.descent, page.descent
lib/python3.12/site-packages/matplotlib/transforms.py ADDED
The diff for this file is too large to render. See raw diff
 
lib/python3.12/site-packages/sglang/srt/__pycache__/code_completion_parser.cpython-312.pyc ADDED
Binary file (5.21 kB). View file
 
lib/python3.12/site-packages/sglang/srt/__pycache__/custom_op.cpython-312.pyc ADDED
Binary file (2.37 kB). View file
 
lib/python3.12/site-packages/sglang/srt/__pycache__/model_parallel.cpython-312.pyc ADDED
Binary file (7.33 kB). View file
 
lib/python3.12/site-packages/sglang/srt/__pycache__/patch_torch.cpython-312.pyc ADDED
Binary file (3.6 kB). View file
 
lib/python3.12/site-packages/sglang/srt/__pycache__/reasoning_parser.cpython-312.pyc ADDED
Binary file (7.57 kB). View file
 
lib/python3.12/site-packages/sglang/srt/__pycache__/utils.cpython-312.pyc ADDED
Binary file (89.2 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/activation.cpython-312.pyc ADDED
Binary file (9.03 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/dp_attention.cpython-312.pyc ADDED
Binary file (9.88 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/elementwise.cpython-312.pyc ADDED
Binary file (19.3 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/layernorm.cpython-312.pyc ADDED
Binary file (9.27 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/linear.cpython-312.pyc ADDED
Binary file (49.1 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/logits_processor.cpython-312.pyc ADDED
Binary file (20.9 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/parameter.cpython-312.pyc ADDED
Binary file (19.5 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/pooler.cpython-312.pyc ADDED
Binary file (3.07 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/radix_attention.cpython-312.pyc ADDED
Binary file (3.55 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/rotary_embedding.cpython-312.pyc ADDED
Binary file (55.5 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/sampler.cpython-312.pyc ADDED
Binary file (12.5 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/torchao_utils.cpython-312.pyc ADDED
Binary file (4.48 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/__pycache__/vocab_parallel_embedding.cpython-312.pyc ADDED
Binary file (24.3 kB). View file
 
lib/python3.12/site-packages/sglang/srt/layers/attention/base_attn_backend.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Optional, Union
5
+
6
+ import torch
7
+
8
+ if TYPE_CHECKING:
9
+ from sglang.srt.layers.radix_attention import RadixAttention
10
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
11
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
12
+
13
+
14
+ class AttentionBackend(ABC):
15
+ """The base class of attention backends"""
16
+
17
+ @abstractmethod
18
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
19
+ """Init the metadata for a forward pass."""
20
+ raise NotImplementedError()
21
+
22
+ def init_cuda_graph_state(self, max_bs: int):
23
+ """Init the global shared states for cuda graph."""
24
+ raise NotImplementedError()
25
+
26
+ def init_forward_metadata_capture_cuda_graph(
27
+ self,
28
+ bs: int,
29
+ num_tokens: int,
30
+ req_pool_indices: torch.Tensor,
31
+ seq_lens: torch.Tensor,
32
+ encoder_lens: Optional[torch.Tensor],
33
+ forward_mode: ForwardMode,
34
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
35
+ ):
36
+ """Init the metadata for a forward pass for capturing a cuda graph."""
37
+ raise NotImplementedError()
38
+
39
+ def init_forward_metadata_replay_cuda_graph(
40
+ self,
41
+ bs: int,
42
+ req_pool_indices: torch.Tensor,
43
+ seq_lens: torch.Tensor,
44
+ seq_lens_sum: int,
45
+ encoder_lens: Optional[torch.Tensor],
46
+ forward_mode: ForwardMode,
47
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
48
+ seq_lens_cpu: Optional[torch.Tensor],
49
+ ):
50
+ """Init the metadata for a forward pass for replaying a cuda graph."""
51
+ raise NotImplementedError()
52
+
53
+ def get_cuda_graph_seq_len_fill_value(self):
54
+ """Get the fill value for padded seq lens. Typically, it is 0 or 1."""
55
+ raise NotImplementedError()
56
+
57
+ def forward(
58
+ self,
59
+ q: torch.Tensor,
60
+ k: torch.Tensor,
61
+ v: torch.Tensor,
62
+ layer: RadixAttention,
63
+ forward_batch: ForwardBatch,
64
+ save_kv_cache: bool = True,
65
+ **kwargs,
66
+ ):
67
+ """Run forward on an attention layer."""
68
+ if forward_batch.forward_mode.is_decode():
69
+ return self.forward_decode(
70
+ q,
71
+ k,
72
+ v,
73
+ layer,
74
+ forward_batch,
75
+ save_kv_cache=save_kv_cache,
76
+ **kwargs,
77
+ )
78
+ else:
79
+ return self.forward_extend(
80
+ q,
81
+ k,
82
+ v,
83
+ layer,
84
+ forward_batch,
85
+ save_kv_cache=save_kv_cache,
86
+ **kwargs,
87
+ )
88
+
89
+ def forward_decode(
90
+ self,
91
+ q: torch.Tensor,
92
+ k: torch.Tensor,
93
+ v: torch.Tensor,
94
+ layer: RadixAttention,
95
+ forward_batch: ForwardBatch,
96
+ save_kv_cache: bool = True,
97
+ ):
98
+ """Run a forward for decode."""
99
+ raise NotImplementedError()
100
+
101
+ def forward_extend(
102
+ self,
103
+ q: torch.Tensor,
104
+ k: torch.Tensor,
105
+ v: torch.Tensor,
106
+ layer: RadixAttention,
107
+ forward_batch: ForwardBatch,
108
+ save_kv_cache: bool = True,
109
+ ):
110
+ """Run a forward for extend."""
111
+ raise NotImplementedError()
lib/python3.12/site-packages/sglang/srt/layers/attention/cutlass_mla_backend.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Support attention backend for Cutlass MLA.
5
+
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING, Optional, Union
10
+
11
+ import torch
12
+ import triton
13
+
14
+ from sglang.global_config import global_config
15
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
16
+ from sglang.srt.layers.attention.flashinfer_mla_backend import FlashInferMLAAttnBackend
17
+ from sglang.srt.layers.attention.utils import create_flashmla_kv_indices_triton
18
+ from sglang.srt.layers.dp_attention import get_attention_tp_size
19
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
20
+ from sglang.srt.utils import is_cuda
21
+
22
+ if TYPE_CHECKING:
23
+ from sglang.srt.layers.radix_attention import RadixAttention
24
+ from sglang.srt.model_executor.model_runner import ModelRunner
25
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
26
+ from sglang.srt.speculative.spec_info import SpecInfo
27
+
28
+ _is_cuda = is_cuda()
29
+ if _is_cuda:
30
+ from sgl_kernel import cutlass_mla_decode, cutlass_mla_get_workspace_size
31
+
32
+
33
+ # Cutlass MLA only supports pagesize=128
34
+ PAGE_SIZE = 128
35
+
36
+
37
+ @dataclass
38
+ class CutlassMLADecodeMetadata:
39
+ workspace: Optional[torch.Tensor] = None
40
+ block_kv_indices: Optional[torch.Tensor] = None
41
+
42
+ def __init__(
43
+ self,
44
+ workspace: Optional[torch.Tensor] = None,
45
+ block_kv_indices: Optional[torch.Tensor] = None,
46
+ ):
47
+ self.workspace = workspace
48
+ self.block_kv_indices = block_kv_indices
49
+
50
+
51
+ class CutlassMLABackend(FlashInferMLAAttnBackend):
52
+ """Cutlass attention kernels."""
53
+
54
+ def __init__(
55
+ self,
56
+ model_runner: ModelRunner,
57
+ skip_prefill: bool = False,
58
+ kv_indptr_buf: Optional[torch.Tensor] = None,
59
+ kv_last_page_len_buf: Optional[torch.Tensor] = None,
60
+ ):
61
+ super().__init__(
62
+ model_runner, skip_prefill, kv_indptr_buf, kv_last_page_len_buf
63
+ )
64
+
65
+ self.num_q_heads = (
66
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
67
+ )
68
+ self.num_kv_heads = model_runner.model_config.get_num_kv_heads(
69
+ get_attention_tp_size()
70
+ )
71
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
72
+ self.num_local_heads = (
73
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
74
+ )
75
+ self.forward_metadata: Union[CutlassMLADecodeMetadata] = None
76
+ self.kv_lora_rank = model_runner.model_config.kv_lora_rank
77
+ self.qk_nope_head_dim = model_runner.model_config.qk_nope_head_dim
78
+ self.qk_rope_head_dim = model_runner.model_config.qk_rope_head_dim
79
+ self.v_head_dim = model_runner.model_config.v_head_dim
80
+ self.scaling = model_runner.model_config.scaling
81
+ self.data_type = model_runner.kv_cache_dtype
82
+ self.q_data_type = model_runner.dtype
83
+ self.kv_cache_dim = self.kv_lora_rank + self.qk_rope_head_dim
84
+
85
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
86
+
87
+ bs = forward_batch.batch_size
88
+ spec_info = forward_batch.spec_info
89
+ if forward_batch.forward_mode.is_decode_or_idle():
90
+ if spec_info is None:
91
+ max_seqlen_pad = triton.cdiv(
92
+ forward_batch.seq_lens_cpu.max().item(), PAGE_SIZE
93
+ )
94
+ block_kv_indices = torch.full(
95
+ (bs, max_seqlen_pad),
96
+ -1,
97
+ dtype=torch.int32,
98
+ device=forward_batch.seq_lens.device,
99
+ )
100
+ create_flashmla_kv_indices_triton[(bs,)](
101
+ self.req_to_token,
102
+ forward_batch.req_pool_indices,
103
+ forward_batch.seq_lens,
104
+ None,
105
+ block_kv_indices,
106
+ self.req_to_token.stride(0),
107
+ max_seqlen_pad,
108
+ PAGE_SIZE,
109
+ )
110
+ workspace_size = cutlass_mla_get_workspace_size(
111
+ max_seqlen_pad * PAGE_SIZE, bs
112
+ )
113
+ workspace = torch.empty(
114
+ workspace_size, device="cuda", dtype=torch.uint8
115
+ )
116
+ self.forward_metadata = CutlassMLADecodeMetadata(
117
+ workspace,
118
+ block_kv_indices,
119
+ )
120
+ else:
121
+ super().init_forward_metadata(forward_batch)
122
+ else:
123
+ super().init_forward_metadata(forward_batch)
124
+
125
+ def init_cuda_graph_state(
126
+ self,
127
+ max_bs: int,
128
+ block_kv_indices: Optional[torch.Tensor] = None,
129
+ ):
130
+ if block_kv_indices is None:
131
+ cuda_graph_kv_indices = torch.full(
132
+ (max_bs, (self.max_context_len + PAGE_SIZE) // PAGE_SIZE),
133
+ 1,
134
+ dtype=torch.int32,
135
+ device="cuda",
136
+ )
137
+ else:
138
+ cuda_graph_kv_indices = block_kv_indices
139
+
140
+ workspace_size = cutlass_mla_get_workspace_size(
141
+ cuda_graph_kv_indices.shape[1] * PAGE_SIZE, max_bs
142
+ )
143
+ self.cuda_graph_mla_workspace = torch.empty(
144
+ workspace_size, device="cuda", dtype=torch.uint8
145
+ )
146
+ self.cuda_graph_kv_indices = cuda_graph_kv_indices
147
+
148
+ def init_forward_metadata_capture_cuda_graph(
149
+ self,
150
+ bs: int,
151
+ num_tokens: int,
152
+ req_pool_indices: torch.Tensor,
153
+ seq_lens: torch.Tensor,
154
+ encoder_lens: Optional[torch.Tensor],
155
+ forward_mode: ForwardMode,
156
+ spec_info: Optional[SpecInfo],
157
+ ):
158
+ if forward_mode.is_decode_or_idle():
159
+ if spec_info is None:
160
+ max_seqlen_pad = triton.cdiv(seq_lens.max().item(), PAGE_SIZE)
161
+
162
+ create_flashmla_kv_indices_triton[(bs,)](
163
+ self.req_to_token,
164
+ req_pool_indices,
165
+ seq_lens,
166
+ None,
167
+ self.cuda_graph_kv_indices,
168
+ self.req_to_token.stride(0),
169
+ self.cuda_graph_kv_indices.stride(0),
170
+ PAGE_SIZE,
171
+ )
172
+ workspace_size = cutlass_mla_get_workspace_size(
173
+ max_seqlen_pad * PAGE_SIZE, bs
174
+ )
175
+ self.cuda_graph_mla_workspace = torch.empty(
176
+ workspace_size, device="cuda", dtype=torch.uint8
177
+ )
178
+ self.forward_metadata = CutlassMLADecodeMetadata(
179
+ self.cuda_graph_mla_workspace,
180
+ self.cuda_graph_kv_indices[:bs, :max_seqlen_pad],
181
+ )
182
+ else:
183
+ super().init_forward_metadata_capture_cuda_graph(
184
+ bs,
185
+ num_tokens,
186
+ req_pool_indices,
187
+ seq_lens,
188
+ encoder_lens,
189
+ forward_mode,
190
+ spec_info,
191
+ )
192
+
193
+ def init_forward_metadata_replay_cuda_graph(
194
+ self,
195
+ bs: int,
196
+ req_pool_indices: torch.Tensor,
197
+ seq_lens: torch.Tensor,
198
+ seq_lens_sum: int,
199
+ encoder_lens: Optional[torch.Tensor],
200
+ forward_mode: ForwardMode,
201
+ spec_info: Optional[SpecInfo],
202
+ seq_lens_cpu: Optional[torch.Tensor],
203
+ ):
204
+
205
+ if forward_mode.is_decode_or_idle():
206
+ assert seq_lens_cpu is not None
207
+ seq_lens = seq_lens[:bs]
208
+ seq_lens_cpu = seq_lens_cpu[:bs]
209
+ max_seqlen_pad = triton.cdiv(seq_lens_cpu.max().item(), PAGE_SIZE)
210
+ create_flashmla_kv_indices_triton[(bs,)](
211
+ self.req_to_token,
212
+ req_pool_indices[:bs],
213
+ seq_lens,
214
+ None,
215
+ self.cuda_graph_kv_indices,
216
+ self.req_to_token.stride(0),
217
+ self.cuda_graph_kv_indices.stride(0),
218
+ PAGE_SIZE,
219
+ )
220
+ workspace_size = cutlass_mla_get_workspace_size(
221
+ max_seqlen_pad * PAGE_SIZE, bs
222
+ )
223
+ self.cuda_graph_mla_workspace = torch.empty(
224
+ workspace_size, device="cuda", dtype=torch.uint8
225
+ )
226
+ self.forward_metadata.workspace = self.cuda_graph_mla_workspace
227
+ self.forward_metadata.block_kv_indices = self.cuda_graph_kv_indices[
228
+ :bs, :max_seqlen_pad
229
+ ]
230
+ else:
231
+ super().init_forward_metadata_replay_cuda_graph(
232
+ bs,
233
+ req_pool_indices,
234
+ seq_lens,
235
+ seq_lens_sum,
236
+ encoder_lens,
237
+ forward_mode,
238
+ spec_info,
239
+ seq_lens_cpu,
240
+ )
241
+
242
+ def get_cuda_graph_seq_len_fill_value(self):
243
+ return 1
244
+
245
+ def forward_decode(
246
+ self,
247
+ q: torch.Tensor,
248
+ k: torch.Tensor,
249
+ v: torch.Tensor,
250
+ layer: RadixAttention,
251
+ forward_batch: ForwardBatch,
252
+ save_kv_cache: bool = True,
253
+ ):
254
+ cache_loc = forward_batch.out_cache_loc
255
+
256
+ if k is not None:
257
+ assert v is not None
258
+ if save_kv_cache:
259
+ forward_batch.token_to_kv_pool.set_kv_buffer(
260
+ layer,
261
+ cache_loc,
262
+ k,
263
+ v,
264
+ )
265
+ bs = forward_batch.batch_size
266
+ k_cache = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
267
+
268
+ reshape_q = q.view(-1, layer.tp_q_head_num, layer.head_dim)
269
+
270
+ o = cutlass_mla_decode(
271
+ q_nope_and_q_pe=reshape_q,
272
+ kv_c_and_k_pe_cache=k_cache.view(-1, PAGE_SIZE, self.kv_cache_dim),
273
+ seq_lens=forward_batch.seq_lens.to(torch.int32),
274
+ page_table=self.forward_metadata.block_kv_indices,
275
+ workspace=self.forward_metadata.workspace,
276
+ )
277
+
278
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
lib/python3.12/site-packages/sglang/srt/layers/attention/double_sparsity_backend.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import torch
6
+
7
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
8
+ from sglang.srt.managers.schedule_batch import global_server_args_dict
9
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch
10
+
11
+ if TYPE_CHECKING:
12
+ from sglang.srt.layers.radix_attention import RadixAttention
13
+ from sglang.srt.model_executor.model_runner import ModelRunner
14
+
15
+
16
+ class DoubleSparseAttnBackend(AttentionBackend):
17
+ def __init__(self, model_runner: ModelRunner):
18
+ # Lazy import to avoid the initialization of cuda context
19
+ from sglang.srt.layers.attention.triton_ops.double_sparsity_attention import (
20
+ extend_attention_fwd,
21
+ flash_decode_attention_fwd,
22
+ flash_decode_sparse_attention_fwd,
23
+ )
24
+
25
+ super().__init__()
26
+
27
+ self.decode_attention_fwd = flash_decode_attention_fwd
28
+ self.decode_sparse_attention_fwd = flash_decode_sparse_attention_fwd
29
+ self.extend_attention_fwd = extend_attention_fwd
30
+ self.num_head = model_runner.model_config.num_attention_heads
31
+ self.head_dim = model_runner.model_config.hidden_size // self.num_head
32
+ self.heavy_token_num = model_runner.server_args.ds_heavy_token_num
33
+
34
+ self.sorted_channels = model_runner.sorted_channels
35
+ self.sparse_decode_thresold = (
36
+ model_runner.server_args.ds_sparse_decode_threshold
37
+ )
38
+ self.att_out_approx: torch.Tensor = None
39
+ self.mid_out: torch.Tensor = None
40
+ self.mid_o_logexpsum: torch.Tensor = None
41
+
42
+ # TODO: Change the hard-coded block_seq_num
43
+ self.BLOCK_SEQ = 128
44
+
45
+ if global_server_args_dict.get("triton_attention_reduce_in_fp32", False):
46
+ self.reduce_dtype = torch.float32
47
+ else:
48
+ self.reduce_dtype = torch.float16
49
+
50
+ self.forward_metadata = None
51
+
52
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
53
+ """Init auxiliary variables for triton attention backend."""
54
+
55
+ if forward_batch.forward_mode.is_decode():
56
+ start_loc = torch.zeros_like(forward_batch.seq_lens, dtype=torch.int32)
57
+ start_loc[1:] = torch.cumsum(forward_batch.seq_lens[:-1], dim=0)
58
+
59
+ total_num_tokens = torch.sum(forward_batch.seq_lens).item()
60
+ attn_logits = torch.empty(
61
+ (self.num_head, total_num_tokens),
62
+ dtype=self.reduce_dtype,
63
+ device="cuda",
64
+ )
65
+
66
+ max_seq_len = torch.max(forward_batch.seq_lens).item()
67
+ min_seq_len = torch.min(forward_batch.seq_lens).item()
68
+ max_extend_len = None
69
+ # NOTE: Align sequence order with req_to_token order
70
+ ds_req_to_token = forward_batch.req_to_token_pool.req_to_token[
71
+ forward_batch.req_pool_indices
72
+ ]
73
+
74
+ bsz = forward_batch.seq_lens.shape[0]
75
+
76
+ att_out_approx = torch.empty(
77
+ [self.num_head, bsz, max_seq_len],
78
+ dtype=self.reduce_dtype,
79
+ device="cuda",
80
+ )
81
+
82
+ block_seq_num = (
83
+ self.heavy_token_num + self.BLOCK_SEQ - 1
84
+ ) // self.BLOCK_SEQ
85
+
86
+ mid_out = torch.empty(
87
+ [bsz, self.num_head, block_seq_num, self.head_dim],
88
+ dtype=torch.float32,
89
+ device="cuda",
90
+ )
91
+ mid_o_logexpsum = torch.empty(
92
+ [bsz, self.num_head, block_seq_num], dtype=torch.float32, device="cuda"
93
+ )
94
+ self.att_out_approx = att_out_approx
95
+ self.mid_out = mid_out
96
+ self.mid_o_logexpsum = mid_o_logexpsum
97
+
98
+ else:
99
+ start_loc = attn_logits = max_seq_len = min_seq_len = None
100
+ prefix_lens = forward_batch.extend_prefix_lens
101
+ max_extend_len = torch.max(forward_batch.seq_lens - prefix_lens).item()
102
+ ds_req_to_token = None
103
+
104
+ self.forward_metadata = (
105
+ start_loc,
106
+ attn_logits,
107
+ max_seq_len,
108
+ min_seq_len,
109
+ max_extend_len,
110
+ ds_req_to_token,
111
+ )
112
+
113
+ def forward_extend(
114
+ self,
115
+ q,
116
+ k,
117
+ v,
118
+ layer: RadixAttention,
119
+ forward_batch: ForwardBatch,
120
+ save_kv_cache=True,
121
+ ):
122
+ # TODO: reuse the buffer across layers
123
+ if layer.qk_head_dim != layer.v_head_dim:
124
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
125
+ else:
126
+ o = torch.empty_like(q)
127
+
128
+ k_label = torch.gather(
129
+ k,
130
+ 2,
131
+ self.sorted_channels[layer.layer_id]
132
+ .unsqueeze(0)
133
+ .expand(k.shape[0], -1, -1),
134
+ )
135
+
136
+ if save_kv_cache:
137
+ forward_batch.token_to_kv_pool.set_kv_buffer(
138
+ layer, forward_batch.out_cache_loc, k, v, k_label
139
+ )
140
+
141
+ (
142
+ start_loc,
143
+ attn_logits,
144
+ max_seq_len,
145
+ min_seq_len,
146
+ max_extend_len,
147
+ ds_req_to_token,
148
+ ) = self.forward_metadata
149
+ self.extend_attention_fwd(
150
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
151
+ k.contiguous(),
152
+ v.contiguous(),
153
+ o.view(-1, layer.tp_q_head_num, layer.v_head_dim),
154
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
155
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
156
+ forward_batch.req_to_token_pool.req_to_token,
157
+ forward_batch.req_pool_indices,
158
+ forward_batch.seq_lens,
159
+ forward_batch.extend_seq_lens,
160
+ forward_batch.extend_start_loc,
161
+ max_extend_len,
162
+ layer.scaling,
163
+ layer.logit_cap,
164
+ )
165
+ return o
166
+
167
+ def forward_decode(
168
+ self,
169
+ q,
170
+ k,
171
+ v,
172
+ layer: RadixAttention,
173
+ forward_batch: ForwardBatch,
174
+ save_kv_cache=True,
175
+ ):
176
+ # During torch.compile, there is a bug in rotary_emb that causes the
177
+ # output value to have a 3D tensor shape. This reshapes the output correctly.
178
+ q = q.reshape(-1, layer.tp_q_head_num * layer.qk_head_dim)
179
+
180
+ # TODO: reuse the buffer across layers
181
+ if layer.qk_head_dim != layer.v_head_dim:
182
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
183
+ else:
184
+ o = torch.empty_like(q)
185
+
186
+ # TODO: Add min seqlen
187
+ (
188
+ start_loc,
189
+ attn_logits,
190
+ max_seq_len,
191
+ min_seq_len,
192
+ max_extend_len,
193
+ ds_req_to_token,
194
+ ) = self.forward_metadata
195
+
196
+ k_label = torch.gather(
197
+ k,
198
+ 2,
199
+ self.sorted_channels[layer.layer_id]
200
+ .unsqueeze(0)
201
+ .expand(k.shape[0], -1, -1),
202
+ )
203
+
204
+ if save_kv_cache:
205
+ forward_batch.token_to_kv_pool.set_kv_buffer(
206
+ layer, forward_batch.out_cache_loc, k, v, k_label
207
+ )
208
+
209
+ # NOTE(Andy) shouldn't be used when max_len_in_batch < heavy_token_num
210
+ # and set a minimum value for sparse_decode
211
+ if (
212
+ min_seq_len < self.heavy_token_num
213
+ or max_seq_len < self.sparse_decode_thresold
214
+ ):
215
+ self.decode_attention_fwd(
216
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
217
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
218
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
219
+ o.view(-1, layer.tp_q_head_num, layer.v_head_dim),
220
+ forward_batch.req_to_token_pool.req_to_token,
221
+ forward_batch.req_pool_indices,
222
+ start_loc,
223
+ forward_batch.seq_lens,
224
+ attn_logits,
225
+ max_seq_len,
226
+ layer.scaling,
227
+ layer.logit_cap,
228
+ )
229
+ else:
230
+ # TODO(Andy): indexing with torch.gather or torch.index_select or customized kernel
231
+ q_label = torch.gather(
232
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
233
+ 2,
234
+ self.sorted_channels[layer.layer_id]
235
+ .unsqueeze(0)
236
+ .expand(q.shape[0], -1, -1),
237
+ )
238
+ self.decode_sparse_attention_fwd(
239
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
240
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
241
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
242
+ o.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
243
+ q_label,
244
+ forward_batch.token_to_kv_pool.get_label_buffer(layer.layer_id),
245
+ ds_req_to_token,
246
+ forward_batch.seq_lens,
247
+ max_seq_len,
248
+ layer.scaling,
249
+ layer.logit_cap,
250
+ self.heavy_token_num,
251
+ self.att_out_approx,
252
+ self.mid_out,
253
+ self.mid_o_logexpsum,
254
+ self.BLOCK_SEQ,
255
+ )
256
+
257
+ return o
lib/python3.12/site-packages/sglang/srt/layers/attention/flashattention_backend.py ADDED
@@ -0,0 +1,1851 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Optional, Union
5
+
6
+ import numpy as np
7
+ import torch
8
+
9
+ from sglang.srt.configs.model_config import AttentionArch
10
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
11
+ from sglang.srt.managers.schedule_batch import global_server_args_dict
12
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
13
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
14
+
15
+ if TYPE_CHECKING:
16
+ from sglang.srt.layers.radix_attention import RadixAttention
17
+ from sglang.srt.model_executor.model_runner import ModelRunner
18
+
19
+ from sgl_kernel import merge_state_v2
20
+ from sgl_kernel.flash_attn import flash_attn_varlen_func, flash_attn_with_kvcache
21
+
22
+
23
+ @dataclass
24
+ class FlashAttentionMetadata:
25
+ """Metadata to be init once in the model forward pass,
26
+ each layer's forward pass can reuse the metadata.
27
+
28
+ For each init metadata function, we will try set up them in below order
29
+ """
30
+
31
+ # Sequence lengths for the forward batch
32
+ cache_seqlens_int32: torch.Tensor = None
33
+ # Maximum sequence length for query
34
+ max_seq_len_q: int = 1
35
+ # Maximum sequence length for key
36
+ max_seq_len_k: int = 0
37
+ # Cumulative sequence lengths for query
38
+ cu_seqlens_q: torch.Tensor = None
39
+ # Cumulative sequence lengths for key
40
+ cu_seqlens_k: torch.Tensor = None
41
+ # Window size (typically used by Gemma)
42
+ window_size: tuple = (-1, -1)
43
+ # Page table, the index of KV Cache Tables/Blocks
44
+ page_table: torch.Tensor = None
45
+
46
+ # Encoder metadata
47
+ # Cumulative sequence lengths for encoder key
48
+ encoder_cu_seqlens_k: torch.Tensor = None
49
+ # Maximum sequence length for encoder key
50
+ encoder_max_seq_len_k: int = 0
51
+ # Sequence lengths for the forward batch
52
+ encoder_lens_int32: torch.Tensor = None
53
+ # Page table for the encoder
54
+ encoder_page_table: torch.Tensor = None
55
+
56
+ @dataclass
57
+ class LocalAttentionMetadata:
58
+ local_query_start_loc: torch.Tensor = None # cu_seqlens_q for local attention
59
+ local_seqused_k: torch.Tensor = None # sequence lengths for local attention
60
+ local_block_table: torch.Tensor = None # block table for local attention
61
+ local_max_query_len: int = 0 # max query length for local attention
62
+ local_max_seq_len: int = 0 # max sequence length for local attention
63
+
64
+ local_attn_metadata: Optional[LocalAttentionMetadata] = None
65
+
66
+
67
+ # Copied from:
68
+ # https://github.com/houseroad/vllm/blob/4e45bfcaf928bdb9bd952b4ac922a3c205589ae8/vllm/v1/attention/backends/flash_attn.py
69
+ #
70
+ # Take in `query_start_loc_np` and `seq_lens_np` and break the sequences into
71
+ # local attention blocks, where each block is passed to the attention kernel
72
+ # as an independent local ("virtual") batch item.
73
+ #
74
+ # For example, if are performing a chunked prefill a batch of 3 sequences:
75
+ # q_seqlens = [4, 10, 5]
76
+ # kv_seqlens = [6, 17, 9]
77
+ # Then normally for regular attention we would compute with an attention mask
78
+ # for batch idx 0 (q_seqlens = 4, kv_seqlens = 6) like:
79
+ # batch idx: 0 (q_seqlens = 4, kv_seqlens = 6)
80
+ # k_toks > 0 1 2 3 4 5
81
+ # q_toks v _____________
82
+ # 0 | 1 1 1
83
+ # 1 | 1 1 1 1
84
+ # 2 | 1 1 1 1 1
85
+ # 3 | 1 1 1 1 1 1
86
+ #
87
+ # for local attention (with attn_chunk_size = 4) we would compute with an
88
+ # attention mask like:
89
+ # batch idx: 0 (q_seqlens = 4, kv_seqlens = 6, attn_chunk_size = 4)
90
+ # k_toks > 0 1 2 3 4 5
91
+ # q_toks v _____________
92
+ # 0 | 1 1 1
93
+ # 1 | 1 1 1 1
94
+ # 2 | 1
95
+ # 3 | 1 1
96
+ #
97
+ # We can simulate this mask using standard flash-attention by breaking the
98
+ # sequences into local ("virtual") batches, where each local batch item is a
99
+ # local attention block, so in this case batch idx 0 would be broken up into:
100
+ #
101
+ # local-batch idx: 0 (q_seqlens = 2, kv_seqlens = 4) (batch 0)
102
+ # k_toks > 0 1 2 3
103
+ # q_toks v _____________
104
+ # 0 | 1 1 1
105
+ # 1 | 1 1 1 1
106
+ # local-batch idx: 1 (q_seqlens = 2, kv_seqlens = 2) (batch 0)
107
+ # k_toks > 4 5
108
+ # q_toks v _____________
109
+ # 2 | 1
110
+ # 3 | 1 1
111
+ #
112
+ # e.g. if we have:
113
+ # attn_chunk_size = 4
114
+ # query_start_loc_np = [0, 4, 14, 19] (q_seqlens = [4, 10, 5])
115
+ # Then this function would return:
116
+ # __b0__ ______b1______ __b2__ < orig batch indices
117
+ # q_seqlens_local = [ 2, 2, 1, 4, 4, 1, 4, 1]
118
+ # cu_seqlens_q_local = [0, 4, 6, 10, 14, 18, 19, 23, 24]
119
+ # seqlens_k_local = [ 4, 2, 4, 4, 4, 1, 4, 1]
120
+ # block_table_local : shape[local_virtual_batches, pages_per_local_batch]
121
+ def make_local_attention_virtual_batches(
122
+ attn_chunk_size: int,
123
+ query_start_loc_np: np.ndarray,
124
+ seq_lens_np: np.ndarray,
125
+ block_table: torch.Tensor,
126
+ page_size: int = 0,
127
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, torch.Tensor]:
128
+ """
129
+ Take in `query_start_loc_np` and `seq_lens_np` and break the sequences into
130
+ local attention blocks, where each block is passed to the attention kernel
131
+ as an independent local ("virtual") batch item.
132
+
133
+ Args:
134
+ attn_chunk_size: Size of local attention chunks
135
+ query_start_loc_np: Cumulative sum of query lengths (numpy array)
136
+ seq_lens_np: Sequence lengths (numpy array)
137
+ block_table: Block table for KV cache
138
+ page_size: Size of each page in the KV cache
139
+
140
+ Returns:
141
+ seqlens_q_local: Query sequence lengths for local attention
142
+ cu_seqlens_q_local: Cumulative sum of query sequence lengths for local attention
143
+ seqlens_k_local: Key sequence lengths for local attention
144
+ block_table_local: Block table for local attention
145
+ """
146
+ # Adjust attention_chunk_size based on the actual sequence length
147
+ # to avoid index out of bounds errors
148
+ max_seq_len = seq_lens_np.max()
149
+ effective_chunk_size = min(attn_chunk_size, max_seq_len)
150
+ # Make sure effective_chunk_size is divisible by page_size
151
+ effective_chunk_size = (effective_chunk_size // page_size) * page_size
152
+ if effective_chunk_size < page_size:
153
+ effective_chunk_size = page_size
154
+ attn_chunk_size = effective_chunk_size
155
+
156
+ q_seqlens = query_start_loc_np[1:] - query_start_loc_np[:-1]
157
+ actual_batch_size = seq_lens_np.shape[0]
158
+
159
+ # Handle if we are starting in the middle of a local attention block,
160
+ # we assume q_seqlens > 0 (for all elements), for each batch idx we compute
161
+ # the number of tokens that are not in the first local attention block and
162
+ # then we can simply use a cdiv for the rest.
163
+ # For example if we have:
164
+ # attn_chunk_size = 4
165
+ # q_seqlens = [4, 10, 5]
166
+ # k_seqlens = [6, 17, 9]
167
+ # Then we would get:
168
+ # new_tokens_in_first_block = [2, 1, 4]
169
+ # local_blocks = [2, 4, 2]
170
+ q_tokens_in_first_block = np.minimum(
171
+ attn_chunk_size - ((seq_lens_np - q_seqlens) % attn_chunk_size), q_seqlens
172
+ ).astype(np.int32)
173
+ tokens_in_last_block = attn_chunk_size + (seq_lens_np % -attn_chunk_size)
174
+ local_blocks = 1 + cdiv(q_seqlens - q_tokens_in_first_block, attn_chunk_size)
175
+
176
+ # Once we know the number of local blocks we can compute the request spans
177
+ # for each batch idx, we can figure out the number of "virtual" requests we
178
+ # have to make,
179
+ # For the above example we would get:
180
+ # seqlens_q_local = [2, 2, 1, 4, 4, 1, 4, 1]
181
+ #
182
+ # First Get batched arange. (E.g., [2, 4, 2] -> [0, 1, 0, 1, 2, 3, 0, 1])
183
+ # (TODO: max a utility to share this code with _prepare_inputs)
184
+ # arange step 1. [2, 4, 2] -> [2, 6, 8]
185
+ cu_num_blocks = np.cumsum(local_blocks)
186
+ virtual_batches = cu_num_blocks[-1]
187
+ # arange step 2. [2, 6, 8] -> [0, 0, 2, 2, 2, 2, 6, 6]
188
+ block_offsets = np.repeat(cu_num_blocks - local_blocks, local_blocks)
189
+ # arange step 3. [0, 1, 0, 1, 2, 3, 0, 1]
190
+ arange = np.arange(virtual_batches, dtype=np.int32) - block_offsets
191
+ # also compute reverse arange (i.e. [1, 0, 3, 2, 1, 0, 1, 0])
192
+ rarange = np.repeat(local_blocks, local_blocks) - arange - 1
193
+ # Then we can compute the seqlens_q_local, handling the fact that the
194
+ # first and last blocks could be partial
195
+ seqlens_q_local = np.repeat(q_seqlens - q_tokens_in_first_block, local_blocks)
196
+ # set the first block since this may be a partial block
197
+ seqlens_q_local[arange == 0] = q_tokens_in_first_block
198
+ # set the remaining blocks
199
+ seqlens_q_local[arange > 0] = np.minimum(
200
+ seqlens_q_local - attn_chunk_size * (arange - 1), attn_chunk_size
201
+ )[arange > 0]
202
+
203
+ # convert from q_seqlens to cu_seqlens_q
204
+ cu_seqlens_q_local = np.pad(np.cumsum(seqlens_q_local), (1, 0)).astype(np.int32)
205
+
206
+ # compute the seqlens_k_local,
207
+ # basically a full local attention block for all but the last block in each
208
+ # batch
209
+ # For our example this will be:
210
+ # seqlens_k_local = [4, 2, 4, 4, 4, 1, 4, 1]
211
+ seqlens_k_local = np.full(cu_num_blocks[-1], attn_chunk_size, dtype=np.int32)
212
+ seqlens_k_local[cu_num_blocks - 1] = tokens_in_last_block
213
+
214
+ k_seqstarts_absolute = np.repeat(seq_lens_np, local_blocks) - (
215
+ rarange * attn_chunk_size + np.repeat(tokens_in_last_block, local_blocks)
216
+ )
217
+ # For the example the local attention blocks start at:
218
+ # _b0_ _____b1_____ _b2_
219
+ # k_seqstarts_absolute = [0, 4, 4, 8, 12, 16, 4, 8]
220
+ block_starts = k_seqstarts_absolute // page_size
221
+
222
+ assert attn_chunk_size % page_size == 0, (
223
+ f"attn_chunk_size {attn_chunk_size} is not "
224
+ f"divisible by page_size {page_size}"
225
+ )
226
+ pages_per_local_batch = attn_chunk_size // page_size
227
+
228
+ # Create a block_table for the local attention blocks
229
+ # For out example if we have a block-table like (assuming page_size=2):
230
+ # block_table = [
231
+ # [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], < batch 0
232
+ # [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], < batch 1
233
+ # [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], < batch 2
234
+ # ]
235
+ # Then for the local batches we would want a block-table like
236
+ # block_table_local = [
237
+ # [ 0, 1 ], < local-batch 0, (batch 0, starting from k[0])
238
+ # [ 2, 3 ], < local-batch 1, (batch 0, starting from k[4])
239
+ # [ 12, 13 ], < local-batch 2, (batch 1, starting from k[4])
240
+ # [ 14, 15 ], < local-batch 3, (batch 1, starting from k[8])
241
+ # [ 16, 17 ], < local-batch 4, (batch 1, starting from k[12])
242
+ # [ 18, 19 ], < local-batch 5, (batch 1, starting from k[16])
243
+ # [ 22, 23 ], < local-batch 6, (batch 2, starting from k[4])
244
+ # [ 24, 25 ], < local-batch 7, (batch 2, starting from k[8])
245
+ # ]
246
+ block_indices = np.broadcast_to(
247
+ np.arange(pages_per_local_batch, dtype=np.int32),
248
+ (virtual_batches, pages_per_local_batch),
249
+ ) + np.expand_dims(block_starts, axis=1)
250
+ # Ensure block_indices doesn't exceed block_table dimensions
251
+ # This is a critical safety check that prevents index out of bounds errors
252
+ # when dealing with large sequences (>8192 tokens) or when the block_table
253
+ # dimensions are smaller than what would be needed for the full attention chunk size.
254
+ block_indices = block_indices.flatten().clip(max=block_table.shape[1] - 1)
255
+ batch_indices = np.repeat(
256
+ np.arange(actual_batch_size, dtype=np.int32),
257
+ local_blocks * pages_per_local_batch,
258
+ )
259
+ block_table_local = block_table[batch_indices, block_indices].view(
260
+ virtual_batches, -1
261
+ )
262
+
263
+ return seqlens_q_local, cu_seqlens_q_local, seqlens_k_local, block_table_local
264
+
265
+
266
+ def cdiv(a: int, b: int) -> int:
267
+ """Ceiling division."""
268
+ return -(a // -b)
269
+
270
+
271
+ # TODO(hebiao064): remove this once we have a better way to handle the merge_state_v2 torch.compile issue
272
+ @torch._dynamo.disable()
273
+ def merge_state_v2_wrapper(o, s_a, o_exp, s_b):
274
+ return merge_state_v2(o, s_a, o_exp, s_b)
275
+
276
+
277
+ class FlashAttentionBackend(AttentionBackend):
278
+ """FlashAttention backend implementation.
279
+
280
+ Note about the init:
281
+ - If no spec decoding
282
+ - FlashAttentionBackend will be init once when the server starts.
283
+ - If spec decoding
284
+ - FlashAttentionBackend will be init once for the target worker
285
+ - FlashAttentionMultiStepBackend will be once for the draft worker
286
+ - It will spawn num_steps FlashAttentionBackend for the draft worker
287
+
288
+ Note about CUDA Graph:
289
+ - We only support CUDA Graph for Decode (Normal Decode and Draft Decode) and Target Verify.
290
+ - We don't support CUDA Graph for Extend and Draft Extend.
291
+ - When server init, init_cuda_graph_state will be called first and then init_cuda_graph_capture will be called.
292
+ - For each forward batch, init_replay_cuda_graph will be called first and then replay the graph.
293
+ """
294
+
295
+ def __init__(
296
+ self,
297
+ model_runner: ModelRunner,
298
+ skip_prefill: bool = False,
299
+ speculative_step_id=0,
300
+ topk=0,
301
+ speculative_num_steps=0,
302
+ ):
303
+ super().__init__()
304
+
305
+ assert not (
306
+ model_runner.sliding_window_size is not None
307
+ and model_runner.model_config.is_encoder_decoder
308
+ ), "Sliding window and cross attention are not supported together"
309
+
310
+ self.forward_metadata: FlashAttentionMetadata = None
311
+ # extra metdata for handling speculative decoding topk > 1, extended draft decode and verify
312
+ self.forward_metadata_spec_decode_expand: FlashAttentionMetadata = None
313
+ self.max_context_len = model_runner.model_config.context_len
314
+ self.device = model_runner.device
315
+ self.decode_cuda_graph_metadata = {}
316
+ self.target_verify_metadata = {}
317
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
318
+ self.kv_cache_dtype = model_runner.kv_cache_dtype
319
+ self.kv_cache_dtype_str = model_runner.server_args.kv_cache_dtype
320
+ self.page_size = model_runner.page_size
321
+ self.use_mla = model_runner.model_config.attention_arch == AttentionArch.MLA
322
+ self.skip_prefill = skip_prefill
323
+ self.topk = model_runner.server_args.speculative_eagle_topk or 0
324
+ self.speculative_num_steps = speculative_num_steps
325
+ self.speculative_num_draft_tokens = (
326
+ model_runner.server_args.speculative_num_draft_tokens
327
+ )
328
+ self.speculative_step_id = speculative_step_id
329
+
330
+ # Local attention settings
331
+ self.attention_chunk_size = (
332
+ model_runner.attention_chunk_size
333
+ if hasattr(model_runner, "attention_chunk_size")
334
+ else None
335
+ )
336
+
337
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
338
+ """Initialize forward metadata hence all layers in the forward pass can reuse it."""
339
+ metadata = FlashAttentionMetadata()
340
+ seqlens_in_batch = forward_batch.seq_lens
341
+ batch_size = len(seqlens_in_batch)
342
+ device = seqlens_in_batch.device
343
+
344
+ if forward_batch.forward_mode.is_decode_or_idle():
345
+ # Draft Decode
346
+ if forward_batch.spec_info is not None:
347
+ if self.topk <= 1:
348
+ metadata.cache_seqlens_int32 = (
349
+ seqlens_in_batch + (self.speculative_step_id + 1)
350
+ ).to(torch.int32)
351
+ metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item() + (
352
+ self.speculative_step_id + 1
353
+ )
354
+ metadata.cu_seqlens_q = torch.arange(
355
+ 0, batch_size + 1, dtype=torch.int32, device=device
356
+ )
357
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
358
+ torch.cumsum(
359
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
360
+ ),
361
+ (1, 0),
362
+ )
363
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
364
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
365
+ ]
366
+ else:
367
+ metadata.cache_seqlens_int32 = (seqlens_in_batch).to(torch.int32)
368
+ metadata.max_seq_len_q = self.topk
369
+ metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item()
370
+ metadata.cu_seqlens_q = torch.arange(
371
+ 0,
372
+ batch_size * self.topk + 1,
373
+ step=self.topk,
374
+ dtype=torch.int32,
375
+ device=device,
376
+ )
377
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
378
+ torch.cumsum(
379
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
380
+ ),
381
+ (1, 0),
382
+ )
383
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
384
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
385
+ ]
386
+
387
+ metadata_expand = FlashAttentionMetadata()
388
+ decode_length = self.speculative_step_id + 1
389
+ metadata_expand.cache_seqlens_int32 = torch.full(
390
+ (seqlens_in_batch.numel() * self.topk,),
391
+ decode_length,
392
+ device=device,
393
+ dtype=torch.int32,
394
+ )
395
+ metadata_expand.max_seq_len_q = 1
396
+ metadata_expand.max_seq_len_k = self.speculative_step_id + 1
397
+ metadata_expand.cu_seqlens_q = torch.arange(
398
+ 0,
399
+ metadata_expand.cache_seqlens_int32.numel() + 1,
400
+ dtype=torch.int32,
401
+ device=device,
402
+ )
403
+ metadata_expand.cu_seqlens_k = torch.arange(
404
+ 0,
405
+ metadata_expand.cache_seqlens_int32.numel() * decode_length + 1,
406
+ step=decode_length,
407
+ dtype=torch.int32,
408
+ device=device,
409
+ )
410
+ cache_loc = forward_batch.out_cache_loc.view(
411
+ self.speculative_num_steps, -1
412
+ ).T.contiguous()
413
+ metadata_expand.page_table = (
414
+ cache_loc[:, :decode_length].contiguous().to(torch.int32)
415
+ )
416
+ self.forward_metadata_spec_decode_expand = metadata_expand
417
+ else:
418
+ # Normal Decode
419
+ metadata.cache_seqlens_int32 = seqlens_in_batch.to(torch.int32)
420
+ metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item()
421
+ metadata.cu_seqlens_q = torch.arange(
422
+ 0, batch_size + 1, dtype=torch.int32, device=device
423
+ )
424
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
425
+ torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)
426
+ )
427
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
428
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
429
+ ]
430
+ # TODO: we need to test this part for llama 4 eagle case
431
+ self._init_local_attn_metadata(metadata, device)
432
+ elif forward_batch.forward_mode.is_target_verify():
433
+ if self.topk <= 1:
434
+ metadata.cache_seqlens_int32 = (
435
+ forward_batch.seq_lens + self.speculative_num_draft_tokens
436
+ ).to(torch.int32)
437
+ metadata.max_seq_len_q = self.speculative_num_draft_tokens
438
+ metadata.max_seq_len_k = (
439
+ forward_batch.seq_lens_cpu.max().item()
440
+ + self.speculative_num_draft_tokens
441
+ )
442
+ metadata.cu_seqlens_q = torch.arange(
443
+ 0,
444
+ batch_size * self.speculative_num_draft_tokens + 1,
445
+ self.speculative_num_draft_tokens,
446
+ dtype=torch.int32,
447
+ device=device,
448
+ )
449
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
450
+ torch.cumsum(
451
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
452
+ ),
453
+ (1, 0),
454
+ )
455
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
456
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
457
+ ]
458
+
459
+ self._init_local_attn_metadata(metadata, device)
460
+ else:
461
+ metadata.cache_seqlens_int32 = forward_batch.seq_lens.to(torch.int32)
462
+ metadata.max_seq_len_q = self.speculative_num_draft_tokens
463
+ metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item()
464
+ metadata.cu_seqlens_q = torch.arange(
465
+ 0,
466
+ batch_size * self.speculative_num_draft_tokens + 1,
467
+ step=self.speculative_num_draft_tokens,
468
+ dtype=torch.int32,
469
+ device=device,
470
+ )
471
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
472
+ torch.cumsum(
473
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
474
+ ),
475
+ (1, 0),
476
+ )
477
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
478
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
479
+ ]
480
+
481
+ metadata_expand = FlashAttentionMetadata()
482
+
483
+ metadata_expand.max_seq_len_q = 1
484
+ metadata_expand.cu_seqlens_q = torch.arange(
485
+ 0,
486
+ forward_batch.seq_lens.numel() * self.speculative_num_draft_tokens
487
+ + 1,
488
+ dtype=torch.int32,
489
+ device=device,
490
+ )
491
+
492
+ # create expand page table
493
+ offsets = torch.arange(
494
+ self.speculative_num_draft_tokens, device=device
495
+ ).unsqueeze(
496
+ 0
497
+ ) # shape: (1, self.speculative_num_draft_tokens)
498
+ cols = offsets.expand(
499
+ forward_batch.seq_lens.numel(), -1
500
+ ) + forward_batch.seq_lens.unsqueeze(1)
501
+ cum_len = torch.nn.functional.pad(
502
+ torch.cumsum(
503
+ (
504
+ forward_batch.seq_lens + self.speculative_num_draft_tokens
505
+ ).repeat_interleave(self.speculative_num_draft_tokens),
506
+ dim=0,
507
+ ),
508
+ (1, 0),
509
+ )[:-1]
510
+ mask_extraction_indices = (
511
+ cols.repeat_interleave(self.speculative_num_draft_tokens, dim=0)
512
+ + cum_len[:, None]
513
+ ).view(1, -1)
514
+ mask = forward_batch.spec_info.custom_mask[
515
+ mask_extraction_indices
516
+ ].view(
517
+ -1, self.speculative_num_draft_tokens
518
+ ) # (bsz * draft_num, draft_num)
519
+
520
+ # shift table indices to avoid padding
521
+ # non_masked_page_table [[8, 9, 10], mask (display with int format) [[1, 0, 0],
522
+ # [8, 9, 10], [1, 1, 0],
523
+ # [8, 9, 10]] [1, 0, 1]]
524
+ # if masked with padding [[8, 0, 0], our mask without padding [[8, 9, 10],
525
+ # [8, 9, 0], [8, 9, 10],
526
+ # [8, 0, 10]] [8, 10, 9]]
527
+ # note here cache_seqlens_int32 is [1, 2, 2] so extra page indices will be ignored in each row
528
+ col_indices = offsets.expand(
529
+ mask.shape[0], self.speculative_num_draft_tokens
530
+ )
531
+ # Build keys: if an entry is valid (mask==True), keep its original index;
532
+ # if not, add self.speculative_num_draft_tokens so that it sorts after all valid entries.
533
+ keys = torch.where(
534
+ mask, col_indices, col_indices + self.speculative_num_draft_tokens
535
+ )
536
+ _, sort_order = torch.sort(keys, dim=1)
537
+ non_masked_page_table = (
538
+ forward_batch.req_to_token_pool.req_to_token[
539
+ forward_batch.req_pool_indices, :
540
+ ]
541
+ .gather(1, cols)
542
+ .repeat_interleave(self.speculative_num_draft_tokens, dim=0)
543
+ ) # (bsz, draft_num)
544
+ metadata_expand.page_table = non_masked_page_table.gather(1, sort_order)
545
+ metadata_expand.cache_seqlens_int32 = mask.sum(dim=1).to(torch.int32)
546
+ metadata_expand.cu_seqlens_k = torch.nn.functional.pad(
547
+ torch.cumsum(
548
+ metadata_expand.cache_seqlens_int32, dim=0, dtype=torch.int32
549
+ ),
550
+ (1, 0),
551
+ )
552
+ metadata_expand.max_seq_len_k = (
553
+ metadata_expand.cache_seqlens_int32.max().item()
554
+ )
555
+ self.forward_metadata_spec_decode_expand = metadata_expand
556
+ elif forward_batch.forward_mode.is_extend_or_draft_extend_or_mixed():
557
+ metadata.cache_seqlens_int32 = seqlens_in_batch.to(torch.int32)
558
+ metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item()
559
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
560
+ torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)
561
+ )
562
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
563
+ forward_batch.req_pool_indices, : metadata.max_seq_len_k
564
+ ]
565
+
566
+ if (
567
+ any(forward_batch.extend_prefix_lens_cpu)
568
+ or forward_batch.forward_mode == ForwardMode.DRAFT_EXTEND
569
+ ):
570
+ extend_seq_lens = forward_batch.extend_seq_lens
571
+ metadata.max_seq_len_q = max(forward_batch.extend_seq_lens_cpu)
572
+ metadata.cu_seqlens_q = torch.nn.functional.pad(
573
+ torch.cumsum(extend_seq_lens, dim=0, dtype=torch.int32), (1, 0)
574
+ )
575
+ else:
576
+ metadata.max_seq_len_q = metadata.max_seq_len_k
577
+ metadata.cu_seqlens_q = metadata.cu_seqlens_k
578
+
579
+ # Setup local attention if enabled
580
+ if forward_batch.forward_mode == ForwardMode.EXTEND:
581
+ self._init_local_attn_metadata(metadata, device)
582
+
583
+ # Encoder metadata for cross attention
584
+ if forward_batch.encoder_lens is not None:
585
+ assert (
586
+ forward_batch.encoder_lens.numel() == 1
587
+ ), "Only encoder size 1 is supported for now"
588
+
589
+ metadata.encoder_lens_int32 = forward_batch.encoder_lens.to(torch.int32)
590
+ metadata.encoder_cu_seqlens_k = torch.nn.functional.pad(
591
+ torch.cumsum(metadata.encoder_lens_int32, dim=0, dtype=torch.int32),
592
+ (1, 0),
593
+ )
594
+ metadata.encoder_max_seq_len_k = metadata.encoder_lens_int32.max().item()
595
+ metadata.encoder_page_table = forward_batch.req_to_token_pool.req_to_token[
596
+ forward_batch.req_pool_indices, : metadata.encoder_max_seq_len_k
597
+ ]
598
+
599
+ # Currently only support forward_batch.encoder_lens.numel() == 1
600
+ metadata.page_table = forward_batch.req_to_token_pool.req_to_token[
601
+ forward_batch.req_pool_indices,
602
+ metadata.encoder_max_seq_len_k : (
603
+ metadata.encoder_max_seq_len_k + metadata.max_seq_len_k
604
+ ),
605
+ ]
606
+
607
+ # Convert the page table to a strided format which is needed by FA3 API
608
+ if self.page_size > 1:
609
+ self.strided_indices = torch.arange(
610
+ 0, metadata.page_table.shape[1], self.page_size, device=self.device
611
+ )
612
+ metadata.page_table = (
613
+ metadata.page_table[:, self.strided_indices] // self.page_size
614
+ )
615
+
616
+ self.forward_metadata = metadata
617
+
618
+ def forward_extend(
619
+ self,
620
+ q: torch.Tensor,
621
+ k: torch.Tensor,
622
+ v: torch.Tensor,
623
+ layer: RadixAttention,
624
+ forward_batch: ForwardBatch,
625
+ save_kv_cache=True,
626
+ # For multi-head latent attention
627
+ q_rope: Optional[torch.Tensor] = None,
628
+ k_rope: Optional[torch.Tensor] = None,
629
+ ):
630
+ if k is not None:
631
+ assert v is not None
632
+ if save_kv_cache:
633
+ cache_loc = (
634
+ forward_batch.out_cache_loc
635
+ if not layer.is_cross_attention
636
+ else forward_batch.encoder_out_cache_loc
637
+ )
638
+ if not self.use_mla:
639
+ forward_batch.token_to_kv_pool.set_kv_buffer(
640
+ layer, cache_loc, k, v, layer.k_scale, layer.v_scale
641
+ )
642
+ else:
643
+ forward_batch.token_to_kv_pool.set_mla_kv_buffer(
644
+ layer,
645
+ cache_loc,
646
+ k,
647
+ k_rope,
648
+ )
649
+
650
+ # Use precomputed metadata across all layers
651
+ metadata = self.forward_metadata
652
+
653
+ # Calculate window size (can be moved to metadata if layer properties don't change)
654
+ # we don't do layer.sliding_window_size - 1 since in model.get_attention_sliding_window_size() we already - 1
655
+ # here is two side inclusive
656
+ window_size = (
657
+ (layer.sliding_window_size, 0)
658
+ if layer.sliding_window_size is not None and layer.sliding_window_size > -1
659
+ else (-1, -1)
660
+ )
661
+ k_descale, v_descale = None, None
662
+ # only use kv scaling if: 1) fp8 kv is explicitly enabled, 2) RadixAttention
663
+ # has corresponding quantization method so that layer.k_scale is not None
664
+ if self.kv_cache_dtype_str != "auto" and layer.k_scale is not None:
665
+ descale_shape = (forward_batch.batch_size, layer.tp_k_head_num)
666
+ k_descale = layer.k_scale.expand(descale_shape)
667
+ v_descale = layer.v_scale.expand(descale_shape)
668
+ q = q.to(self.kv_cache_dtype)
669
+ causal = not layer.is_cross_attention
670
+
671
+ # Check if we should use local attention
672
+ use_local_attn = (
673
+ self.attention_chunk_size is not None
674
+ and metadata.local_attn_metadata is not None
675
+ and (hasattr(layer, "use_irope") and layer.use_irope)
676
+ )
677
+
678
+ # We do cascade attention for Target Verify with topk > 1
679
+ use_cascade_attn = (
680
+ forward_batch.forward_mode.is_target_verify() and self.topk > 1
681
+ )
682
+
683
+ # Get the appropriate page table based on whether we're using local attention
684
+ if use_local_attn:
685
+ local_metadata = metadata.local_attn_metadata
686
+ page_table = local_metadata.local_block_table
687
+ cu_seqlens_q = local_metadata.local_query_start_loc
688
+ cache_seqlens = local_metadata.local_seqused_k
689
+ max_seqlen_q = local_metadata.local_max_query_len
690
+ max_seqlen_k = local_metadata.local_max_seq_len
691
+ else:
692
+ page_table = metadata.page_table
693
+ cu_seqlens_q = metadata.cu_seqlens_q
694
+ cache_seqlens = metadata.cache_seqlens_int32
695
+ max_seqlen_q = metadata.max_seq_len_q
696
+ max_seqlen_k = metadata.max_seq_len_k
697
+ cu_seqlens_k = metadata.cu_seqlens_k
698
+
699
+ # Use Flash Attention for prefill
700
+ if not self.use_mla:
701
+ # Do multi-head attention
702
+ key_cache, value_cache = forward_batch.token_to_kv_pool.get_kv_buffer(
703
+ layer.layer_id
704
+ )
705
+ key_cache = key_cache.view(
706
+ -1, self.page_size, layer.tp_k_head_num, layer.head_dim
707
+ )
708
+ value_cache = value_cache.view(
709
+ -1, self.page_size, layer.tp_v_head_num, layer.head_dim
710
+ )
711
+ if layer.is_cross_attention:
712
+ page_table = metadata.encoder_page_table
713
+ cache_seqlens = metadata.encoder_lens_int32
714
+ cu_seqlens_k = metadata.encoder_cu_seqlens_k
715
+ window_size = (-1, -1)
716
+
717
+ result = flash_attn_with_kvcache(
718
+ q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
719
+ k_cache=key_cache,
720
+ v_cache=value_cache,
721
+ page_table=page_table,
722
+ cache_seqlens=cache_seqlens,
723
+ cu_seqlens_q=cu_seqlens_q,
724
+ cu_seqlens_k_new=cu_seqlens_k if not use_local_attn else None,
725
+ max_seqlen_q=max_seqlen_q,
726
+ softmax_scale=layer.scaling,
727
+ causal=False if use_cascade_attn else causal,
728
+ window_size=window_size,
729
+ softcap=layer.logit_cap,
730
+ k_descale=k_descale,
731
+ v_descale=v_descale,
732
+ return_softmax_lse=use_cascade_attn,
733
+ )
734
+
735
+ if use_cascade_attn:
736
+ o, softmax_lse, *rest = result
737
+ o_expand, softmax_lse_expand, *rest_expand = flash_attn_with_kvcache(
738
+ q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
739
+ k_cache=key_cache,
740
+ v_cache=value_cache,
741
+ page_table=self.forward_metadata_spec_decode_expand.page_table,
742
+ cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
743
+ cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
744
+ cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
745
+ max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
746
+ softmax_scale=layer.scaling,
747
+ causal=False,
748
+ window_size=window_size,
749
+ softcap=layer.logit_cap,
750
+ k_descale=k_descale,
751
+ v_descale=v_descale,
752
+ return_softmax_lse=True,
753
+ )
754
+ o, _ = merge_state_v2_wrapper(
755
+ o,
756
+ softmax_lse.T.contiguous(),
757
+ o_expand,
758
+ softmax_lse_expand.T.contiguous(),
759
+ )
760
+ else:
761
+ o = result
762
+ else:
763
+ if (
764
+ not global_server_args_dict["disable_chunked_prefix_cache"]
765
+ and forward_batch.attn_attend_prefix_cache is not None
766
+ and not forward_batch.forward_mode.is_target_verify()
767
+ and not forward_batch.forward_mode.is_draft_extend()
768
+ ):
769
+ # Do multi-head attention with chunked prefix cache
770
+
771
+ if forward_batch.attn_attend_prefix_cache:
772
+ # MHA for chunked prefix kv cache when running model with MLA
773
+ assert forward_batch.prefix_chunk_idx is not None
774
+ assert forward_batch.prefix_chunk_cu_seq_lens is not None
775
+ assert forward_batch.prefix_chunk_max_seq_lens is not None
776
+
777
+ chunk_idx = forward_batch.prefix_chunk_idx
778
+ assert chunk_idx >= 0
779
+
780
+ output, lse, *rest = flash_attn_varlen_func(
781
+ q=q.view(-1, layer.tp_q_head_num, layer.head_dim),
782
+ k=k.view(-1, layer.tp_k_head_num, layer.head_dim),
783
+ v=v.view(-1, layer.tp_k_head_num, layer.v_head_dim),
784
+ cu_seqlens_q=metadata.cu_seqlens_q,
785
+ cu_seqlens_k=forward_batch.prefix_chunk_cu_seq_lens[chunk_idx],
786
+ max_seqlen_q=metadata.max_seq_len_q,
787
+ max_seqlen_k=forward_batch.prefix_chunk_max_seq_lens[chunk_idx],
788
+ softmax_scale=layer.scaling,
789
+ causal=False,
790
+ return_softmax_lse=True,
791
+ )
792
+ else:
793
+ # MHA for extend part of sequence without attending prefix kv cache
794
+ output, lse, *rest = flash_attn_varlen_func(
795
+ q=q.view(-1, layer.tp_q_head_num, layer.head_dim),
796
+ k=k.view(-1, layer.tp_k_head_num, layer.head_dim),
797
+ v=v.view(-1, layer.tp_k_head_num, layer.v_head_dim),
798
+ cu_seqlens_q=metadata.cu_seqlens_q,
799
+ cu_seqlens_k=metadata.cu_seqlens_q,
800
+ max_seqlen_q=metadata.max_seq_len_q,
801
+ max_seqlen_k=metadata.max_seq_len_q,
802
+ softmax_scale=layer.scaling,
803
+ causal=True,
804
+ return_softmax_lse=True,
805
+ )
806
+ return output, lse
807
+ else:
808
+ # Do absorbed multi-latent attention
809
+ kv_cache = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
810
+ k_rope = kv_cache[:, :, layer.v_head_dim :]
811
+ c_kv = kv_cache[:, :, : layer.v_head_dim]
812
+ k_rope_cache = k_rope.view(
813
+ -1,
814
+ self.page_size,
815
+ layer.tp_k_head_num,
816
+ layer.head_dim - layer.v_head_dim,
817
+ )
818
+ c_kv_cache = c_kv.view(
819
+ -1, self.page_size, layer.tp_v_head_num, layer.v_head_dim
820
+ )
821
+ if q_rope is not None:
822
+ q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
823
+ q_rope = q_rope.view(
824
+ -1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
825
+ )
826
+ else:
827
+ q_all = q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim)
828
+ q_nope = q_all[:, :, : layer.v_head_dim]
829
+ q_rope = q_all[:, :, layer.v_head_dim :]
830
+
831
+ result = flash_attn_with_kvcache(
832
+ q=q_rope,
833
+ k_cache=k_rope_cache,
834
+ v_cache=c_kv_cache,
835
+ qv=q_nope,
836
+ page_table=page_table,
837
+ cache_seqlens=cache_seqlens,
838
+ cu_seqlens_q=cu_seqlens_q,
839
+ cu_seqlens_k_new=cu_seqlens_k if not use_local_attn else None,
840
+ max_seqlen_q=max_seqlen_q,
841
+ softmax_scale=layer.scaling,
842
+ causal=False if use_cascade_attn else causal,
843
+ softcap=layer.logit_cap,
844
+ k_descale=k_descale,
845
+ v_descale=v_descale,
846
+ return_softmax_lse=use_cascade_attn,
847
+ )
848
+ if use_cascade_attn:
849
+ o, softmax_lse, *rest = result
850
+ o_expand, softmax_lse_expand, *rest_expand = (
851
+ flash_attn_with_kvcache(
852
+ q=q_rope,
853
+ k_cache=k_rope_cache,
854
+ v_cache=c_kv_cache,
855
+ qv=q_nope,
856
+ page_table=self.forward_metadata_spec_decode_expand.page_table,
857
+ cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
858
+ cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
859
+ cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
860
+ max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
861
+ softmax_scale=layer.scaling,
862
+ causal=False,
863
+ window_size=window_size,
864
+ softcap=layer.logit_cap,
865
+ k_descale=k_descale,
866
+ v_descale=v_descale,
867
+ return_softmax_lse=True,
868
+ )
869
+ )
870
+ o, _ = merge_state_v2_wrapper(
871
+ o,
872
+ softmax_lse.T.contiguous(),
873
+ o_expand,
874
+ softmax_lse_expand.T.contiguous(),
875
+ )
876
+ else:
877
+ o = result
878
+
879
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
880
+
881
+ def forward_decode(
882
+ self,
883
+ q: torch.Tensor,
884
+ k: torch.Tensor,
885
+ v: torch.Tensor,
886
+ layer: RadixAttention,
887
+ forward_batch: ForwardBatch,
888
+ save_kv_cache=True,
889
+ # For multi-head latent attention
890
+ q_rope: Optional[torch.Tensor] = None,
891
+ k_rope: Optional[torch.Tensor] = None,
892
+ ) -> torch.Tensor:
893
+ if k is not None:
894
+ assert v is not None
895
+ if save_kv_cache:
896
+ cache_loc = (
897
+ forward_batch.out_cache_loc
898
+ if not layer.is_cross_attention
899
+ else forward_batch.encoder_out_cache_loc
900
+ )
901
+ if not self.use_mla:
902
+ forward_batch.token_to_kv_pool.set_kv_buffer(
903
+ layer, cache_loc, k, v, layer.k_scale, layer.v_scale
904
+ )
905
+ else:
906
+ forward_batch.token_to_kv_pool.set_mla_kv_buffer(
907
+ layer,
908
+ cache_loc,
909
+ k,
910
+ k_rope,
911
+ )
912
+
913
+ # Use precomputed metadata across all layers
914
+ metadata = self.forward_metadata
915
+ local_attn_metadata = getattr(metadata, "local_attn_metadata", None)
916
+ use_local_attention = (
917
+ self.attention_chunk_size is not None and local_attn_metadata is not None
918
+ )
919
+ # We do cascade attention for Draft Decode with topk > 1
920
+ use_cascade_attn = self.topk > 1
921
+
922
+ # Calculate window size (can be moved to metadata if layer properties don't change)
923
+ # we don't do layer.sliding_window_size - 1 since in model.get_attention_sliding_window_size() we already - 1
924
+ # here is two side inclusive
925
+ window_size = (
926
+ (layer.sliding_window_size, 0)
927
+ if layer.sliding_window_size is not None and layer.sliding_window_size > -1
928
+ else (-1, -1)
929
+ )
930
+ causal = not layer.is_cross_attention
931
+
932
+ k_descale, v_descale = None, None
933
+ # only use kv scaling if: 1) fp8 kv is explicitly enabled, 2) RadixAttention
934
+ # has corresponding quantization method so that layer.k_scale is not None
935
+ if self.kv_cache_dtype_str != "auto":
936
+ if layer.k_scale is not None:
937
+ descale_shape = (forward_batch.batch_size, layer.tp_k_head_num)
938
+ k_descale = layer.k_scale.expand(descale_shape)
939
+ v_descale = layer.v_scale.expand(descale_shape)
940
+ q = q.to(self.kv_cache_dtype)
941
+
942
+ if not self.use_mla:
943
+ # Do multi-head attention
944
+
945
+ key_cache, value_cache = forward_batch.token_to_kv_pool.get_kv_buffer(
946
+ layer.layer_id
947
+ )
948
+ key_cache = key_cache.view(
949
+ -1, self.page_size, layer.tp_k_head_num, layer.head_dim
950
+ )
951
+ value_cache = value_cache.view(
952
+ -1, self.page_size, layer.tp_v_head_num, layer.head_dim
953
+ )
954
+
955
+ if layer.is_cross_attention:
956
+ # Always use non-chunked logic for cross-attention
957
+ o = flash_attn_with_kvcache(
958
+ q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
959
+ k_cache=key_cache,
960
+ v_cache=value_cache,
961
+ page_table=metadata.encoder_page_table,
962
+ cache_seqlens=metadata.encoder_lens_int32,
963
+ cu_seqlens_q=metadata.cu_seqlens_q,
964
+ cu_seqlens_k_new=metadata.encoder_cu_seqlens_k,
965
+ max_seqlen_q=1,
966
+ softmax_scale=layer.scaling,
967
+ causal=False,
968
+ window_size=(-1, -1),
969
+ softcap=layer.logit_cap,
970
+ k_descale=k_descale,
971
+ v_descale=v_descale,
972
+ )
973
+ elif use_local_attention:
974
+ # Use chunked (local) attention batching for self-attention
975
+ o = flash_attn_with_kvcache(
976
+ q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
977
+ k_cache=key_cache,
978
+ v_cache=value_cache,
979
+ page_table=local_attn_metadata.local_block_table,
980
+ cache_seqlens=local_attn_metadata.local_seqused_k,
981
+ cu_seqlens_q=local_attn_metadata.local_query_start_loc,
982
+ cu_seqlens_k_new=metadata.cu_seqlens_k,
983
+ max_seqlen_q=local_attn_metadata.local_max_query_len,
984
+ softmax_scale=layer.scaling,
985
+ causal=True,
986
+ window_size=(-1, -1),
987
+ softcap=layer.logit_cap,
988
+ k_descale=k_descale,
989
+ v_descale=v_descale,
990
+ )
991
+ else:
992
+ page_table = metadata.page_table
993
+ cache_seqlens = metadata.cache_seqlens_int32
994
+ cu_seqlens_k = metadata.cu_seqlens_k
995
+ max_seqlen_q = metadata.max_seq_len_q
996
+ q_reshaped = q.contiguous().view(
997
+ -1, layer.tp_q_head_num, layer.head_dim
998
+ )
999
+
1000
+ # Default: single-token self-attention
1001
+ result = flash_attn_with_kvcache(
1002
+ q=q_reshaped,
1003
+ k_cache=key_cache,
1004
+ v_cache=value_cache,
1005
+ page_table=page_table,
1006
+ cache_seqlens=cache_seqlens,
1007
+ cu_seqlens_q=metadata.cu_seqlens_q,
1008
+ cu_seqlens_k_new=cu_seqlens_k,
1009
+ max_seqlen_q=max_seqlen_q,
1010
+ softmax_scale=layer.scaling,
1011
+ causal=False if use_cascade_attn else causal,
1012
+ window_size=window_size,
1013
+ softcap=layer.logit_cap,
1014
+ k_descale=k_descale,
1015
+ v_descale=v_descale,
1016
+ return_softmax_lse=use_cascade_attn,
1017
+ )
1018
+ if use_cascade_attn:
1019
+ o, softmax_lse, *rest = result
1020
+ o_expand, softmax_lse_expand, *rest_expand = (
1021
+ flash_attn_with_kvcache(
1022
+ q=q_reshaped,
1023
+ k_cache=key_cache,
1024
+ v_cache=value_cache,
1025
+ page_table=self.forward_metadata_spec_decode_expand.page_table,
1026
+ cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
1027
+ cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
1028
+ cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
1029
+ max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
1030
+ softmax_scale=layer.scaling,
1031
+ causal=False,
1032
+ window_size=window_size,
1033
+ softcap=layer.logit_cap,
1034
+ k_descale=k_descale,
1035
+ v_descale=v_descale,
1036
+ return_softmax_lse=True,
1037
+ )
1038
+ )
1039
+ o, _ = merge_state_v2(
1040
+ o,
1041
+ softmax_lse.T.contiguous(),
1042
+ o_expand,
1043
+ softmax_lse_expand.T.contiguous(),
1044
+ )
1045
+ else:
1046
+ o = result
1047
+ else:
1048
+ # Do absorbed multi-latent attention
1049
+ kv_cache = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
1050
+ k_rope = kv_cache[:, :, layer.v_head_dim :]
1051
+ c_kv = kv_cache[:, :, : layer.v_head_dim]
1052
+ k_rope_cache = k_rope.view(
1053
+ -1,
1054
+ self.page_size,
1055
+ layer.tp_k_head_num,
1056
+ layer.head_dim - layer.v_head_dim,
1057
+ )
1058
+ c_kv_cache = c_kv.view(
1059
+ -1, self.page_size, layer.tp_v_head_num, layer.v_head_dim
1060
+ )
1061
+
1062
+ if q_rope is not None:
1063
+ q_nope = q.view(-1, layer.tp_q_head_num, layer.v_head_dim)
1064
+ q_rope = q_rope.view(
1065
+ -1, layer.tp_q_head_num, layer.head_dim - layer.v_head_dim
1066
+ )
1067
+ else:
1068
+ q_all = q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim)
1069
+ q_nope = q_all[:, :, : layer.v_head_dim]
1070
+ q_rope = q_all[:, :, layer.v_head_dim :]
1071
+ max_seqlen_q = metadata.max_seq_len_q
1072
+
1073
+ result = flash_attn_with_kvcache(
1074
+ q=q_rope,
1075
+ k_cache=k_rope_cache,
1076
+ v_cache=c_kv_cache,
1077
+ qv=q_nope,
1078
+ page_table=metadata.page_table,
1079
+ cache_seqlens=metadata.cache_seqlens_int32,
1080
+ cu_seqlens_q=metadata.cu_seqlens_q,
1081
+ cu_seqlens_k_new=metadata.cu_seqlens_k,
1082
+ max_seqlen_q=max_seqlen_q,
1083
+ softmax_scale=layer.scaling,
1084
+ causal=False if use_cascade_attn else causal,
1085
+ softcap=layer.logit_cap,
1086
+ k_descale=k_descale,
1087
+ v_descale=v_descale,
1088
+ return_softmax_lse=use_cascade_attn, # softmax_lse is needed for merge states
1089
+ )
1090
+ if use_cascade_attn:
1091
+ o, softmax_lse, *rest = result
1092
+ o_expand, softmax_lse_expand, *rest_expand = flash_attn_with_kvcache(
1093
+ q=q_rope,
1094
+ k_cache=k_rope_cache,
1095
+ v_cache=c_kv_cache,
1096
+ qv=q_nope,
1097
+ page_table=self.forward_metadata_spec_decode_expand.page_table,
1098
+ cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
1099
+ cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
1100
+ cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
1101
+ max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
1102
+ softmax_scale=layer.scaling,
1103
+ causal=False,
1104
+ window_size=window_size,
1105
+ softcap=layer.logit_cap,
1106
+ k_descale=k_descale,
1107
+ v_descale=v_descale,
1108
+ return_softmax_lse=True,
1109
+ )
1110
+ o, _ = merge_state_v2(
1111
+ o,
1112
+ softmax_lse.T.contiguous(),
1113
+ o_expand,
1114
+ softmax_lse_expand.T.contiguous(),
1115
+ )
1116
+ else:
1117
+ o = result
1118
+
1119
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
1120
+
1121
+ def init_cuda_graph_state(self, max_bs: int):
1122
+ """Initialize CUDA graph state for the attention backend.
1123
+
1124
+ Args:
1125
+ max_bs (int): Maximum batch size to support in CUDA graphs
1126
+
1127
+ This creates fixed-size tensors that will be reused during CUDA graph replay
1128
+ to avoid memory allocations.
1129
+ """
1130
+
1131
+ # This is being used by normal decode and draft decode when topk == 1
1132
+ self.decode_cuda_graph_metadata = {
1133
+ "cache_seqlens": torch.zeros(max_bs, dtype=torch.int32, device=self.device),
1134
+ "cu_seqlens_q": torch.arange(
1135
+ 0, max_bs + 1, dtype=torch.int32, device=self.device
1136
+ ),
1137
+ "cu_seqlens_k": torch.zeros(
1138
+ max_bs + 1, dtype=torch.int32, device=self.device
1139
+ ),
1140
+ "page_table": torch.zeros(
1141
+ max_bs,
1142
+ (self.max_context_len + self.page_size - 1) // self.page_size,
1143
+ dtype=torch.int32,
1144
+ device=self.device,
1145
+ ),
1146
+ "page_table_draft_decode": torch.zeros(
1147
+ max_bs,
1148
+ (self.max_context_len + self.page_size - 1) // self.page_size,
1149
+ dtype=torch.int32,
1150
+ device=self.device,
1151
+ ),
1152
+ "strided_indices": torch.arange(
1153
+ 0, self.max_context_len, self.page_size, device=self.device
1154
+ ),
1155
+ }
1156
+
1157
+ # This is used by draft decode's first half of metadata when topk > 1
1158
+ if self.topk > 1:
1159
+ self.draft_decode_metadata_topk_normal = {
1160
+ "cache_seqlens": torch.zeros(
1161
+ max_bs, dtype=torch.int32, device=self.device
1162
+ ),
1163
+ "cu_seqlens_q": torch.arange(
1164
+ 0,
1165
+ max_bs * self.topk + 1,
1166
+ step=self.topk,
1167
+ dtype=torch.int32,
1168
+ device=self.device,
1169
+ ),
1170
+ "cu_seqlens_k": torch.zeros(
1171
+ max_bs + 1, dtype=torch.int32, device=self.device
1172
+ ),
1173
+ "page_table": torch.zeros(
1174
+ max_bs,
1175
+ self.max_context_len,
1176
+ dtype=torch.int32,
1177
+ device=self.device,
1178
+ ),
1179
+ }
1180
+
1181
+ # This is used by draft decode's second half of metadata when topk > 1
1182
+ decode_length = self.speculative_step_id + 1
1183
+ self.draft_decode_metadata_topk_expand = {
1184
+ "cache_seqlens": torch.full(
1185
+ (max_bs * self.topk,),
1186
+ decode_length,
1187
+ device=self.device,
1188
+ dtype=torch.int32,
1189
+ ),
1190
+ "cu_seqlens_q": torch.arange(
1191
+ 0,
1192
+ max_bs * self.topk + 1,
1193
+ dtype=torch.int32,
1194
+ device=self.device,
1195
+ ),
1196
+ "cu_seqlens_k": torch.arange(
1197
+ 0,
1198
+ max_bs * self.topk * decode_length + 1,
1199
+ step=decode_length,
1200
+ dtype=torch.int32,
1201
+ device=self.device,
1202
+ ),
1203
+ "page_table": torch.zeros(
1204
+ max_bs * self.topk,
1205
+ decode_length,
1206
+ dtype=torch.int32,
1207
+ device=self.device,
1208
+ ),
1209
+ }
1210
+
1211
+ if (
1212
+ self.speculative_num_draft_tokens is not None
1213
+ and self.speculative_num_draft_tokens > 0
1214
+ ):
1215
+ self.target_verify_metadata = {
1216
+ "cache_seqlens": torch.zeros(
1217
+ max_bs, dtype=torch.int32, device=self.device
1218
+ ),
1219
+ "cu_seqlens_q": torch.arange(
1220
+ 0,
1221
+ max_bs * self.speculative_num_draft_tokens + 1,
1222
+ step=self.speculative_num_draft_tokens,
1223
+ dtype=torch.int32,
1224
+ device=self.device,
1225
+ ),
1226
+ "cu_seqlens_k": torch.zeros(
1227
+ max_bs + 1, dtype=torch.int32, device=self.device
1228
+ ),
1229
+ "page_table": torch.zeros(
1230
+ max_bs,
1231
+ (self.max_context_len + self.page_size - 1) // self.page_size,
1232
+ dtype=torch.int32,
1233
+ device=self.device,
1234
+ ),
1235
+ "strided_indices": torch.arange(
1236
+ 0, self.max_context_len, self.page_size, device=self.device
1237
+ ),
1238
+ }
1239
+
1240
+ if self.topk > 1:
1241
+ self.target_verify_metadata_topk_normal = {
1242
+ "cache_seqlens": torch.zeros(
1243
+ max_bs, dtype=torch.int32, device=self.device
1244
+ ),
1245
+ "cu_seqlens_q": torch.arange(
1246
+ 0,
1247
+ max_bs * self.speculative_num_draft_tokens + 1,
1248
+ step=self.speculative_num_draft_tokens,
1249
+ dtype=torch.int32,
1250
+ device=self.device,
1251
+ ),
1252
+ "cu_seqlens_k": torch.zeros(
1253
+ max_bs + 1, dtype=torch.int32, device=self.device
1254
+ ),
1255
+ "page_table": torch.zeros(
1256
+ max_bs,
1257
+ self.max_context_len,
1258
+ dtype=torch.int32,
1259
+ device=self.device,
1260
+ ),
1261
+ }
1262
+
1263
+ self.target_verify_metadata_topk_expand = {
1264
+ "cache_seqlens": torch.zeros(
1265
+ max_bs * self.speculative_num_draft_tokens,
1266
+ dtype=torch.int32,
1267
+ device=self.device,
1268
+ ),
1269
+ "cu_seqlens_k": torch.zeros(
1270
+ max_bs * self.speculative_num_draft_tokens + 1,
1271
+ dtype=torch.int32,
1272
+ device=self.device,
1273
+ ),
1274
+ "cu_seqlens_q": torch.arange(
1275
+ 0,
1276
+ max_bs * self.speculative_num_draft_tokens + 1,
1277
+ dtype=torch.int32,
1278
+ device=self.device,
1279
+ ),
1280
+ "page_table": torch.zeros(
1281
+ max_bs * self.speculative_num_draft_tokens,
1282
+ self.speculative_num_draft_tokens,
1283
+ dtype=torch.int32,
1284
+ device=self.device,
1285
+ ),
1286
+ }
1287
+
1288
+ self.encoder_metadata = {
1289
+ "encoder_page_table": torch.zeros(
1290
+ max_bs,
1291
+ self.max_context_len,
1292
+ dtype=torch.int32,
1293
+ device=self.device,
1294
+ ),
1295
+ "encoder_lens_int32": torch.zeros(
1296
+ max_bs, dtype=torch.int32, device=self.device
1297
+ ),
1298
+ "encoder_cu_seqlens_k": torch.zeros(
1299
+ max_bs + 1, dtype=torch.int32, device=self.device
1300
+ ),
1301
+ }
1302
+
1303
+ def init_forward_metadata_capture_cuda_graph(
1304
+ self,
1305
+ bs: int,
1306
+ num_tokens: int,
1307
+ req_pool_indices: torch.Tensor,
1308
+ seq_lens: torch.Tensor,
1309
+ encoder_lens: Optional[torch.Tensor],
1310
+ forward_mode: ForwardMode,
1311
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
1312
+ ):
1313
+ """Initialize forward metadata for capturing CUDA graph."""
1314
+ metadata = FlashAttentionMetadata()
1315
+
1316
+ # metadata_expand is needed for Spec Decoding when top k > 1
1317
+ metadata_expand = FlashAttentionMetadata()
1318
+
1319
+ device = seq_lens.device
1320
+ if forward_mode.is_decode_or_idle():
1321
+ if spec_info is not None:
1322
+ # Draft Decode
1323
+ if self.topk <= 1:
1324
+ # When topk = 1, we use the normal decode metadata
1325
+ metadata.cache_seqlens_int32 = self.decode_cuda_graph_metadata[
1326
+ "cache_seqlens"
1327
+ ][:bs]
1328
+ metadata.max_seq_len_k = seq_lens.max().item() + (
1329
+ self.speculative_step_id + 1
1330
+ )
1331
+ metadata.cu_seqlens_q = self.decode_cuda_graph_metadata[
1332
+ "cu_seqlens_q"
1333
+ ][: bs + 1]
1334
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
1335
+ torch.cumsum(
1336
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
1337
+ ),
1338
+ (1, 0),
1339
+ )
1340
+ metadata.page_table = self.decode_cuda_graph_metadata[
1341
+ "page_table_draft_decode"
1342
+ ][req_pool_indices, :]
1343
+ self.decode_cuda_graph_metadata[bs] = metadata
1344
+ else:
1345
+ # When top k > 1, we need two specific draft decode metadata, and then merge states
1346
+ # 1. The first half of metadata for prefix tokens
1347
+ metadata.cache_seqlens_int32 = (
1348
+ self.draft_decode_metadata_topk_normal["cache_seqlens"][:bs]
1349
+ )
1350
+ metadata.max_seq_len_q = self.topk
1351
+ metadata.max_seq_len_k = seq_lens.max().item()
1352
+ metadata.cu_seqlens_q = self.draft_decode_metadata_topk_normal[
1353
+ "cu_seqlens_q"
1354
+ ][: bs + 1]
1355
+ metadata.cu_seqlens_k = self.draft_decode_metadata_topk_normal[
1356
+ "cu_seqlens_k"
1357
+ ][: bs + 1]
1358
+ metadata.page_table = self.draft_decode_metadata_topk_normal[
1359
+ "page_table"
1360
+ ][req_pool_indices, :]
1361
+
1362
+ # 2. The second half of metadata for draft tokens (per_batch_num_tokens = topk)
1363
+ metadata_expand.cache_seqlens_int32 = (
1364
+ self.draft_decode_metadata_topk_expand["cache_seqlens"][
1365
+ : bs * self.topk
1366
+ ]
1367
+ )
1368
+ metadata_expand.max_seq_len_q = 1
1369
+ metadata_expand.max_seq_len_k = (
1370
+ self.speculative_step_id + 1
1371
+ ) # , do this in replay
1372
+ metadata_expand.cu_seqlens_q = (
1373
+ self.draft_decode_metadata_topk_expand["cu_seqlens_q"][
1374
+ : bs * self.topk + 1
1375
+ ]
1376
+ )
1377
+ metadata_expand.cu_seqlens_k = (
1378
+ self.draft_decode_metadata_topk_expand["cu_seqlens_k"][
1379
+ : bs * self.topk + 1
1380
+ ]
1381
+ )
1382
+ metadata_expand.page_table = self.draft_decode_metadata_topk_expand[
1383
+ "page_table"
1384
+ ][: bs * self.topk]
1385
+ self.draft_decode_metadata_topk_normal[bs] = metadata
1386
+ self.draft_decode_metadata_topk_expand[bs] = metadata_expand
1387
+ else:
1388
+ # Normal Decode
1389
+ # Get sequence information
1390
+ metadata.cache_seqlens_int32 = seq_lens.to(torch.int32)
1391
+ batch_size = len(seq_lens)
1392
+ device = seq_lens.device
1393
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
1394
+ torch.cumsum(seq_lens, dim=0, dtype=torch.int32), (1, 0)
1395
+ )
1396
+ # Precompute maximum sequence length
1397
+ metadata.max_seq_len_k = seq_lens.max().item()
1398
+ # Precompute page table
1399
+ metadata.page_table = self.decode_cuda_graph_metadata["page_table"][
1400
+ req_pool_indices, :
1401
+ ]
1402
+ # Precompute cumulative sequence lengths
1403
+ metadata.cu_seqlens_q = torch.arange(
1404
+ 0, batch_size + 1, dtype=torch.int32, device=device
1405
+ )
1406
+ self.decode_cuda_graph_metadata[bs] = metadata
1407
+
1408
+ elif forward_mode.is_target_verify():
1409
+ if self.topk <= 1:
1410
+ metadata.cache_seqlens_int32 = self.target_verify_metadata[
1411
+ "cache_seqlens"
1412
+ ][:bs]
1413
+ metadata.cache_seqlens_int32.copy_(
1414
+ (seq_lens + self.speculative_num_draft_tokens).to(torch.int32)
1415
+ )
1416
+
1417
+ metadata.max_seq_len_q = self.speculative_num_draft_tokens
1418
+ metadata.max_seq_len_k = (
1419
+ seq_lens.max().item() + self.speculative_num_draft_tokens
1420
+ )
1421
+
1422
+ metadata.cu_seqlens_q = torch.arange(
1423
+ 0,
1424
+ bs * self.speculative_num_draft_tokens + 1,
1425
+ self.speculative_num_draft_tokens,
1426
+ dtype=torch.int32,
1427
+ device=device,
1428
+ )
1429
+
1430
+ metadata.cu_seqlens_k = self.target_verify_metadata["cu_seqlens_k"][
1431
+ : (bs + 1)
1432
+ ]
1433
+
1434
+ metadata.page_table = self.target_verify_metadata["page_table"][
1435
+ req_pool_indices, :
1436
+ ]
1437
+
1438
+ self.target_verify_metadata[bs] = metadata
1439
+ else:
1440
+ # When topk > 1, we need two specific target verify metadata, and then merge states
1441
+ # 1. The first half of metadata for prefix tokens
1442
+ metadata.cache_seqlens_int32 = self.target_verify_metadata_topk_normal[
1443
+ "cache_seqlens"
1444
+ ][:bs]
1445
+ metadata.max_seq_len_q = self.speculative_num_draft_tokens
1446
+ # metadata.max_seq_len_k = forward_batch.seq_lens_cpu.max().item(), do this in replay
1447
+ metadata.cu_seqlens_q = self.target_verify_metadata_topk_normal[
1448
+ "cu_seqlens_q"
1449
+ ][: bs + 1]
1450
+ metadata.cu_seqlens_k = self.target_verify_metadata_topk_normal[
1451
+ "cu_seqlens_k"
1452
+ ][: bs + 1]
1453
+ metadata.page_table = self.target_verify_metadata_topk_normal[
1454
+ "page_table"
1455
+ ][req_pool_indices, :]
1456
+
1457
+ # 2. The second half of metadata for draft tokens (per_batch_num_tokens = topk)
1458
+ metadata_expand.cache_seqlens_int32 = (
1459
+ self.target_verify_metadata_topk_expand["cache_seqlens"][
1460
+ : bs * self.speculative_num_draft_tokens
1461
+ ]
1462
+ )
1463
+ metadata_expand.max_seq_len_q = 1
1464
+ metadata_expand.cu_seqlens_q = self.target_verify_metadata_topk_expand[
1465
+ "cu_seqlens_q"
1466
+ ][: bs * self.speculative_num_draft_tokens + 1]
1467
+ metadata_expand.cu_seqlens_k = self.target_verify_metadata_topk_expand[
1468
+ "cu_seqlens_k"
1469
+ ][: bs * self.speculative_num_draft_tokens + 1]
1470
+
1471
+ metadata_expand.page_table = self.target_verify_metadata_topk_expand[
1472
+ "page_table"
1473
+ ][: bs * self.speculative_num_draft_tokens]
1474
+
1475
+ self.target_verify_metadata_topk_normal[bs] = metadata
1476
+ self.target_verify_metadata_topk_expand[bs] = metadata_expand
1477
+
1478
+ if encoder_lens is not None:
1479
+ encoder_bs = encoder_lens.numel()
1480
+ metadata.encoder_lens_int32 = self.encoder_metadata["encoder_lens_int32"][
1481
+ :encoder_bs
1482
+ ]
1483
+ metadata.encoder_cu_seqlens_k = self.encoder_metadata[
1484
+ "encoder_cu_seqlens_k"
1485
+ ][: (encoder_bs + 1)]
1486
+
1487
+ metadata.encoder_page_table = self.encoder_metadata["encoder_page_table"][
1488
+ req_pool_indices, :
1489
+ ]
1490
+
1491
+ self.forward_metadata = metadata
1492
+ self.forward_metadata_spec_decode_expand = metadata_expand
1493
+
1494
+ def init_forward_metadata_replay_cuda_graph(
1495
+ self,
1496
+ bs: int,
1497
+ req_pool_indices: torch.Tensor,
1498
+ seq_lens: torch.Tensor,
1499
+ seq_lens_sum: int,
1500
+ encoder_lens: Optional[torch.Tensor],
1501
+ forward_mode: ForwardMode,
1502
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
1503
+ seq_lens_cpu: Optional[torch.Tensor],
1504
+ out_cache_loc: torch.Tensor = None,
1505
+ ):
1506
+ """Initialize forward metadata for replaying CUDA graph."""
1507
+ seq_lens = seq_lens[:bs]
1508
+ seq_lens_cpu = seq_lens_cpu[:bs]
1509
+ req_pool_indices = req_pool_indices[:bs]
1510
+ device = seq_lens.device
1511
+ metadata = None
1512
+ metadata_expand = None
1513
+
1514
+ if forward_mode.is_decode_or_idle():
1515
+
1516
+ if spec_info is not None:
1517
+ # Draft Decode
1518
+ if self.topk <= 1:
1519
+ metadata = self.decode_cuda_graph_metadata[bs]
1520
+ # When topk = 1, we use the normal decode metadata
1521
+ metadata.cache_seqlens_int32.copy_(
1522
+ (seq_lens + (self.speculative_step_id + 1)).to(torch.int32)
1523
+ )
1524
+
1525
+ metadata.max_seq_len_k = seq_lens_cpu.max().item() + (
1526
+ self.speculative_step_id + 1
1527
+ )
1528
+ metadata.cu_seqlens_k.copy_(
1529
+ torch.nn.functional.pad(
1530
+ torch.cumsum(
1531
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
1532
+ ),
1533
+ (1, 0),
1534
+ )
1535
+ )
1536
+
1537
+ max_seq_pages = (
1538
+ metadata.max_seq_len_k + self.page_size - 1
1539
+ ) // self.page_size
1540
+ page_indices = self.req_to_token[
1541
+ req_pool_indices[:, None],
1542
+ self.decode_cuda_graph_metadata["strided_indices"][
1543
+ :max_seq_pages
1544
+ ],
1545
+ ]
1546
+
1547
+ page_indices //= self.page_size
1548
+ metadata.page_table[:, :max_seq_pages].copy_(page_indices)
1549
+ else:
1550
+ # When top k > 1, we need two specific draft decode metadata, and then merge states
1551
+ # 1. The first half of metadata for prefix tokens
1552
+ metadata = self.draft_decode_metadata_topk_normal[bs]
1553
+ metadata.cache_seqlens_int32.copy_(seq_lens.to(torch.int32))
1554
+ # metadata.max_seq_len_q = self.topk, already set in capture
1555
+ metadata.max_seq_len_k = seq_lens_cpu.max().item()
1556
+ # metadata.cu_seqlens_q already set in capture
1557
+ metadata.cu_seqlens_k.copy_(
1558
+ torch.nn.functional.pad(
1559
+ torch.cumsum(
1560
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
1561
+ ),
1562
+ (1, 0),
1563
+ )
1564
+ )
1565
+
1566
+ page_table = self.req_to_token[
1567
+ req_pool_indices, : metadata.max_seq_len_k
1568
+ ]
1569
+
1570
+ metadata.page_table[:, : metadata.max_seq_len_k].copy_(page_table)
1571
+
1572
+ # 2. The second half of metadata for draft tokens (per_batch_num_tokens = topk)
1573
+ metadata_expand = self.draft_decode_metadata_topk_expand[bs]
1574
+ decode_length = self.speculative_step_id + 1
1575
+ cache_loc = out_cache_loc.view(
1576
+ self.speculative_num_steps, -1
1577
+ ).T.contiguous()
1578
+ metadata_expand.page_table[: cache_loc.shape[0]].copy_(
1579
+ cache_loc[:, :decode_length].contiguous().to(torch.int32)
1580
+ )
1581
+ # TODO: we need to test this part for llama 4 eagle case
1582
+ self._init_local_attn_metadata(metadata, device)
1583
+ else:
1584
+ metadata = self.decode_cuda_graph_metadata[bs]
1585
+ # Normal Decode
1586
+ max_len = seq_lens_cpu.max().item()
1587
+ metadata.max_seq_len_k = max_len
1588
+
1589
+ metadata.cache_seqlens_int32 = seq_lens.to(torch.int32)
1590
+ metadata.cu_seqlens_k = torch.nn.functional.pad(
1591
+ torch.cumsum(seq_lens, dim=0, dtype=torch.int32), (1, 0)
1592
+ )
1593
+
1594
+ max_seq_pages = (
1595
+ metadata.max_seq_len_k + self.page_size - 1
1596
+ ) // self.page_size
1597
+ page_indices = self.req_to_token[
1598
+ req_pool_indices[:, None],
1599
+ self.decode_cuda_graph_metadata["strided_indices"][:max_seq_pages][
1600
+ None, :
1601
+ ],
1602
+ ]
1603
+ page_indices //= self.page_size
1604
+ metadata.page_table[:, :max_seq_pages].copy_(page_indices)
1605
+ metadata.page_table[:, max_seq_pages:].fill_(0)
1606
+
1607
+ self._init_local_attn_metadata(metadata, device)
1608
+ elif forward_mode.is_target_verify():
1609
+ if self.topk <= 1:
1610
+ metadata = self.target_verify_metadata[bs]
1611
+ metadata.cache_seqlens_int32.copy_(
1612
+ (seq_lens + self.speculative_num_draft_tokens).to(torch.int32)
1613
+ )
1614
+
1615
+ metadata.max_seq_len_k = (
1616
+ seq_lens_cpu.max().item() + self.speculative_num_draft_tokens
1617
+ )
1618
+ metadata.cu_seqlens_k.copy_(
1619
+ torch.nn.functional.pad(
1620
+ torch.cumsum(
1621
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
1622
+ ),
1623
+ (1, 0),
1624
+ )
1625
+ )
1626
+ max_seq_pages = (
1627
+ metadata.max_seq_len_k + self.page_size - 1
1628
+ ) // self.page_size
1629
+ page_indices = self.req_to_token[
1630
+ req_pool_indices[:, None],
1631
+ self.decode_cuda_graph_metadata["strided_indices"][:max_seq_pages],
1632
+ ]
1633
+ page_indices //= self.page_size
1634
+ metadata.page_table[:, :max_seq_pages].copy_(page_indices)
1635
+ else:
1636
+ # When topk > 1, we need two specific target verify metadata, and then merge states
1637
+ # 1. The first half of metadata for prefix tokens
1638
+ metadata = self.target_verify_metadata_topk_normal[bs]
1639
+ metadata.cache_seqlens_int32.copy_(seq_lens.to(torch.int32))
1640
+ # metadata.max_seq_len_q = self.speculative_num_draft_tokens, already set in capture
1641
+ metadata.max_seq_len_k = seq_lens_cpu.max().item()
1642
+ # metadata.cu_seqlens_q already set in capture
1643
+ metadata.cu_seqlens_k.copy_(
1644
+ torch.nn.functional.pad(
1645
+ torch.cumsum(
1646
+ metadata.cache_seqlens_int32, dim=0, dtype=torch.int32
1647
+ ),
1648
+ (1, 0),
1649
+ )
1650
+ )
1651
+ page_table = self.req_to_token[
1652
+ req_pool_indices, : metadata.max_seq_len_k
1653
+ ]
1654
+ metadata.page_table[:, : metadata.max_seq_len_k].copy_(page_table)
1655
+
1656
+ # 2. The second half of metadata for draft tokens (per_batch_num_tokens = topk)
1657
+ metadata_expand = self.target_verify_metadata_topk_expand[bs]
1658
+ # metadata_expand.max_seq_len_q = 1, already set in capture
1659
+ # metadata_expand.cu_seqlens_q already set in capture
1660
+
1661
+ offsets = torch.arange(
1662
+ self.speculative_num_draft_tokens, device=device
1663
+ ).unsqueeze(
1664
+ 0
1665
+ ) # shape: (1, self.speculative_num_draft_tokens)
1666
+ cols = offsets.expand(seq_lens.numel(), -1) + seq_lens.unsqueeze(1)
1667
+ cum_len = torch.nn.functional.pad(
1668
+ torch.cumsum(
1669
+ (
1670
+ seq_lens + self.speculative_num_draft_tokens
1671
+ ).repeat_interleave(self.speculative_num_draft_tokens),
1672
+ dim=0,
1673
+ ),
1674
+ (1, 0),
1675
+ )[:-1]
1676
+ mask_extraction_indices = (
1677
+ cols.repeat_interleave(self.speculative_num_draft_tokens, dim=0)
1678
+ + cum_len[:, None]
1679
+ ).view(1, -1)
1680
+ # avoid extracting padded seq indices which will be out of boundary
1681
+ mask_extraction_indices[
1682
+ :, spec_info.positions.numel() * self.speculative_num_draft_tokens :
1683
+ ].fill_(0)
1684
+
1685
+ mask = spec_info.custom_mask[mask_extraction_indices].view(
1686
+ -1, self.speculative_num_draft_tokens
1687
+ ) # (bsz * draft_num, draft_num)
1688
+ col_indices = offsets.expand(
1689
+ mask.shape[0], self.speculative_num_draft_tokens
1690
+ )
1691
+ keys = torch.where(
1692
+ mask, col_indices, col_indices + self.speculative_num_draft_tokens
1693
+ )
1694
+ _, sort_order = torch.sort(keys, dim=1)
1695
+
1696
+ non_masked_page_table = (
1697
+ self.req_to_token[req_pool_indices, :]
1698
+ .gather(1, cols)
1699
+ .repeat_interleave(self.speculative_num_draft_tokens, dim=0)
1700
+ ) # (bsz, draft_num)
1701
+ metadata_expand.page_table.copy_(
1702
+ non_masked_page_table.gather(1, sort_order)
1703
+ )
1704
+ metadata_expand.cache_seqlens_int32.copy_(
1705
+ mask.sum(dim=1).to(torch.int32)
1706
+ )
1707
+ metadata_expand.cu_seqlens_k.copy_(
1708
+ torch.nn.functional.pad(
1709
+ torch.cumsum(
1710
+ metadata_expand.cache_seqlens_int32,
1711
+ dim=0,
1712
+ dtype=torch.int32,
1713
+ ),
1714
+ (1, 0),
1715
+ )
1716
+ )
1717
+ metadata_expand.max_seq_len_k = (
1718
+ metadata_expand.cache_seqlens_int32.max().item()
1719
+ )
1720
+
1721
+ if encoder_lens is not None:
1722
+ # Only support encoder size 1 for now
1723
+ metadata.encoder_max_seq_len_k = encoder_lens[0]
1724
+ metadata.encoder_lens_int32.copy_(encoder_lens[:1])
1725
+ metadata.encoder_cu_seqlens_k.copy_(
1726
+ torch.nn.functional.pad(
1727
+ torch.cumsum(metadata.encoder_lens_int32, dim=0, dtype=torch.int32),
1728
+ (1, 0),
1729
+ )
1730
+ )
1731
+
1732
+ metadata.encoder_page_table[:, : metadata.encoder_max_seq_len_k].copy_(
1733
+ self.req_to_token[req_pool_indices, : metadata.encoder_max_seq_len_k]
1734
+ )
1735
+
1736
+ # Update the regular page table
1737
+ page_table = self.req_to_token[
1738
+ req_pool_indices,
1739
+ metadata.encoder_max_seq_len_k : (
1740
+ metadata.encoder_max_seq_len_k + metadata.max_seq_len_k
1741
+ ),
1742
+ ]
1743
+ metadata.page_table[:, : metadata.max_seq_len_k].copy_(page_table)
1744
+
1745
+ self.forward_metadata = metadata
1746
+ self.forward_metadata_spec_decode_expand = metadata_expand
1747
+
1748
+ def get_cuda_graph_seq_len_fill_value(self):
1749
+ """Get the fill value for sequence length in CUDA graph."""
1750
+ return 0
1751
+
1752
+ def _init_local_attn_metadata(self, metadata: FlashAttentionMetadata, device):
1753
+ """Centralized utility to initialize local_attn_metadata if chunked attention is enabled."""
1754
+ if self.attention_chunk_size is None:
1755
+ metadata.local_attn_metadata = None
1756
+ return
1757
+
1758
+ cu_seqlens_q = metadata.cu_seqlens_q
1759
+ cache_seqlens_int32 = metadata.cache_seqlens_int32
1760
+ page_table = metadata.page_table
1761
+ if cu_seqlens_q is None or cache_seqlens_int32 is None or page_table is None:
1762
+ metadata.local_attn_metadata = None
1763
+ return
1764
+
1765
+ cu_seqlens_q_np = cu_seqlens_q.cpu().numpy()
1766
+ seq_lens_np = cache_seqlens_int32.cpu().numpy()
1767
+ (
1768
+ seqlens_q_local_np,
1769
+ cu_seqlens_q_local_np,
1770
+ seqlens_k_local_np,
1771
+ block_table_local,
1772
+ ) = make_local_attention_virtual_batches(
1773
+ self.attention_chunk_size,
1774
+ cu_seqlens_q_np,
1775
+ seq_lens_np,
1776
+ page_table,
1777
+ self.page_size,
1778
+ )
1779
+ local_metadata = FlashAttentionMetadata.LocalAttentionMetadata(
1780
+ local_query_start_loc=torch.from_numpy(cu_seqlens_q_local_np).to(device),
1781
+ local_seqused_k=torch.from_numpy(seqlens_k_local_np).to(device),
1782
+ local_block_table=block_table_local.to(device),
1783
+ local_max_query_len=int(seqlens_q_local_np.max()),
1784
+ local_max_seq_len=int(seqlens_k_local_np.max()),
1785
+ )
1786
+ metadata.local_attn_metadata = local_metadata
1787
+
1788
+
1789
+ class FlashAttentionMultiStepBackend:
1790
+
1791
+ def __init__(
1792
+ self, model_runner: ModelRunner, topk: int, speculative_num_steps: int
1793
+ ):
1794
+ self.model_runner = model_runner
1795
+ self.topk = topk
1796
+ self.speculative_num_steps = speculative_num_steps
1797
+ self.attn_backends = []
1798
+ for i in range(self.speculative_num_steps):
1799
+ self.attn_backends.append(
1800
+ FlashAttentionBackend(
1801
+ model_runner,
1802
+ speculative_step_id=i,
1803
+ topk=self.topk,
1804
+ speculative_num_steps=self.speculative_num_steps,
1805
+ )
1806
+ )
1807
+
1808
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
1809
+ for i in range(self.speculative_num_steps - 1):
1810
+ self.attn_backends[i].init_forward_metadata(forward_batch)
1811
+
1812
+ def init_cuda_graph_state(self, max_bs: int):
1813
+ for i in range(self.speculative_num_steps):
1814
+ self.attn_backends[i].init_cuda_graph_state(max_bs)
1815
+
1816
+ def init_forward_metadata_capture_cuda_graph(
1817
+ self,
1818
+ forward_batch: ForwardBatch,
1819
+ ):
1820
+ assert forward_batch.spec_info is not None
1821
+ assert isinstance(forward_batch.spec_info, EagleDraftInput)
1822
+
1823
+ for i in range(self.speculative_num_steps - 1):
1824
+ self.attn_backends[i].init_forward_metadata_capture_cuda_graph(
1825
+ forward_batch.batch_size,
1826
+ forward_batch.batch_size * self.topk,
1827
+ forward_batch.req_pool_indices,
1828
+ forward_batch.seq_lens,
1829
+ encoder_lens=forward_batch.encoder_lens,
1830
+ forward_mode=ForwardMode.DECODE,
1831
+ spec_info=forward_batch.spec_info,
1832
+ )
1833
+
1834
+ def init_forward_metadata_replay_cuda_graph(
1835
+ self, forward_batch: ForwardBatch, bs: int
1836
+ ):
1837
+ assert forward_batch.spec_info is not None
1838
+ assert isinstance(forward_batch.spec_info, EagleDraftInput)
1839
+
1840
+ for i in range(self.speculative_num_steps - 1):
1841
+ self.attn_backends[i].init_forward_metadata_replay_cuda_graph(
1842
+ bs,
1843
+ forward_batch.req_pool_indices,
1844
+ forward_batch.seq_lens,
1845
+ forward_batch.seq_lens_sum,
1846
+ encoder_lens=forward_batch.encoder_lens,
1847
+ forward_mode=ForwardMode.DECODE,
1848
+ spec_info=forward_batch.spec_info,
1849
+ seq_lens_cpu=forward_batch.seq_lens_cpu,
1850
+ out_cache_loc=forward_batch.out_cache_loc,
1851
+ )
lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_backend.py ADDED
@@ -0,0 +1,1266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Support different attention backends.
5
+ Now there are two backends: FlashInfer and Triton.
6
+ FlashInfer is faster and Triton is easier to customize.
7
+ Each backend supports two operators: extend (i.e. prefill with cached prefix) and decode.
8
+ """
9
+
10
+ import os
11
+ from dataclasses import dataclass
12
+ from enum import Enum, auto
13
+ from functools import partial
14
+ from typing import TYPE_CHECKING, Callable, List, Optional, Union
15
+
16
+ import torch
17
+
18
+ from sglang.global_config import global_config
19
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
20
+ from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
21
+ from sglang.srt.layers.dp_attention import get_attention_tp_size
22
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
23
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
24
+ from sglang.srt.utils import is_flashinfer_available, next_power_of_2
25
+
26
+ if TYPE_CHECKING:
27
+ from sglang.srt.layers.radix_attention import RadixAttention
28
+ from sglang.srt.model_executor.model_runner import ModelRunner
29
+
30
+ if is_flashinfer_available():
31
+ from flashinfer import (
32
+ BatchDecodeWithPagedKVCacheWrapper,
33
+ BatchPrefillWithPagedKVCacheWrapper,
34
+ BatchPrefillWithRaggedKVCacheWrapper,
35
+ )
36
+ from flashinfer.cascade import merge_state
37
+ from flashinfer.decode import _get_range_buf, get_seq_lens
38
+
39
+
40
+ class WrapperDispatch(Enum):
41
+ SLIDING_WINDOW = auto()
42
+ CROSS_ATTENTION = auto()
43
+
44
+
45
+ @dataclass
46
+ class DecodeMetadata:
47
+ decode_wrappers: List[BatchDecodeWithPagedKVCacheWrapper]
48
+
49
+
50
+ @dataclass
51
+ class PrefillMetadata:
52
+ prefill_wrappers: List[BatchPrefillWithPagedKVCacheWrapper]
53
+ use_ragged: bool
54
+ extend_no_prefix: bool
55
+
56
+
57
+ # Reuse this workspace buffer across all flashinfer wrappers
58
+ global_workspace_buffer = None
59
+
60
+
61
+ class FlashInferAttnBackend(AttentionBackend):
62
+ """Flashinfer attention kernels."""
63
+
64
+ def __init__(
65
+ self,
66
+ model_runner: ModelRunner,
67
+ skip_prefill: bool = False,
68
+ kv_indptr_buf: Optional[torch.Tensor] = None,
69
+ kv_last_page_len_buf: Optional[torch.Tensor] = None,
70
+ ):
71
+ super().__init__()
72
+
73
+ # Parse constants
74
+ self.decode_use_tensor_cores = should_use_tensor_core(
75
+ kv_cache_dtype=model_runner.kv_cache_dtype,
76
+ num_attention_heads=model_runner.model_config.num_attention_heads
77
+ // get_attention_tp_size(),
78
+ num_kv_heads=model_runner.model_config.get_num_kv_heads(
79
+ get_attention_tp_size()
80
+ ),
81
+ )
82
+ self.max_context_len = model_runner.model_config.context_len
83
+ self.skip_prefill = skip_prefill
84
+ self.is_multimodal = model_runner.model_config.is_multimodal
85
+ self.kv_cache_dtype = model_runner.kv_cache_dtype
86
+ self.kv_cache_dtype_str = model_runner.server_args.kv_cache_dtype
87
+
88
+ assert not (
89
+ model_runner.sliding_window_size is not None
90
+ and model_runner.model_config.is_encoder_decoder
91
+ ), "Sliding window and cross attention are not supported together"
92
+
93
+ if model_runner.sliding_window_size is not None:
94
+ self.num_wrappers = 2
95
+ self.dispatch_reason = WrapperDispatch.SLIDING_WINDOW
96
+ elif model_runner.model_config.is_encoder_decoder:
97
+ self.num_wrappers = 2
98
+ self.dispatch_reason = WrapperDispatch.CROSS_ATTENTION
99
+ else:
100
+ self.num_wrappers = 1
101
+ self.dispatch_reason = None
102
+
103
+ # Qwen2/Qwen3 models require higher flashinfer workspace size
104
+ if (
105
+ "Qwen2ForCausalLM" in model_runner.model_config.hf_config.architectures
106
+ or "Qwen3ForCausalLM" in model_runner.model_config.hf_config.architectures
107
+ ):
108
+ global_config.flashinfer_workspace_size = 512 * 1024 * 1024
109
+
110
+ # Allocate buffers
111
+ global global_workspace_buffer
112
+ if global_workspace_buffer is None:
113
+ global_workspace_buffer = torch.empty(
114
+ global_config.flashinfer_workspace_size,
115
+ dtype=torch.uint8,
116
+ device=model_runner.device,
117
+ )
118
+ self.workspace_buffer = global_workspace_buffer
119
+ max_bs = model_runner.req_to_token_pool.size
120
+ if kv_indptr_buf is None:
121
+ self.kv_indptr = [
122
+ torch.zeros(
123
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
124
+ )
125
+ for _ in range(self.num_wrappers)
126
+ ]
127
+ else:
128
+ assert self.num_wrappers == 1
129
+ self.kv_indptr = [kv_indptr_buf]
130
+
131
+ if kv_last_page_len_buf is None:
132
+ self.kv_last_page_len = torch.ones(
133
+ (max_bs,), dtype=torch.int32, device=model_runner.device
134
+ )
135
+ else:
136
+ assert self.num_wrappers == 1
137
+ self.kv_last_page_len = kv_last_page_len_buf
138
+
139
+ if not self.skip_prefill:
140
+ self.qo_indptr = [
141
+ torch.zeros(
142
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
143
+ )
144
+ for _ in range(self.num_wrappers)
145
+ ]
146
+
147
+ self.prefill_wrapper_ragged = BatchPrefillWithRaggedKVCacheWrapper(
148
+ self.workspace_buffer, "NHD"
149
+ )
150
+
151
+ # Two wrappers: one for sliding window attention and one for full attention.
152
+ # Using two wrappers is unnecessary in the current PR, but are prepared for future PRs
153
+ self.prefill_wrappers_paged = []
154
+ self.prefill_wrappers_verify = []
155
+ self.decode_wrappers = []
156
+ for _ in range(self.num_wrappers):
157
+ if not skip_prefill:
158
+ self.prefill_wrappers_paged.append(
159
+ BatchPrefillWithPagedKVCacheWrapper(
160
+ self.workspace_buffer,
161
+ "NHD",
162
+ backend="fa2",
163
+ )
164
+ )
165
+ self.prefill_wrappers_verify.append(
166
+ BatchPrefillWithPagedKVCacheWrapper(
167
+ self.workspace_buffer,
168
+ "NHD",
169
+ )
170
+ )
171
+ self.decode_wrappers.append(
172
+ BatchDecodeWithPagedKVCacheWrapper(
173
+ self.workspace_buffer,
174
+ "NHD",
175
+ use_tensor_cores=self.decode_use_tensor_cores,
176
+ )
177
+ )
178
+
179
+ # Create indices updater
180
+ if not skip_prefill:
181
+ self.indices_updater_prefill = FlashInferIndicesUpdaterPrefill(
182
+ model_runner, self
183
+ ) # for verify
184
+ self.indices_updater_decode = FlashInferIndicesUpdaterDecode(model_runner, self)
185
+
186
+ # Other metadata
187
+ self.forward_metadata: Union[PrefillMetadata, DecodeMetadata] = None
188
+ self.decode_cuda_graph_metadata = {}
189
+ self.prefill_cuda_graph_metadata = {} # For verify
190
+ self.draft_extend_cuda_graph_metadata = {} # For draft extend
191
+
192
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
193
+ if forward_batch.forward_mode.is_decode_or_idle():
194
+ self.indices_updater_decode.update(
195
+ forward_batch.req_pool_indices,
196
+ forward_batch.seq_lens,
197
+ forward_batch.seq_lens_sum,
198
+ decode_wrappers=self.decode_wrappers,
199
+ encoder_lens=forward_batch.encoder_lens,
200
+ spec_info=forward_batch.spec_info,
201
+ )
202
+ self.forward_metadata = DecodeMetadata(self.decode_wrappers)
203
+ elif forward_batch.forward_mode.is_draft_extend():
204
+ self.indices_updater_prefill.update(
205
+ forward_batch.req_pool_indices,
206
+ forward_batch.seq_lens,
207
+ forward_batch.seq_lens_sum,
208
+ prefix_lens=None,
209
+ prefill_wrappers=self.prefill_wrappers_paged,
210
+ use_ragged=False,
211
+ encoder_lens=forward_batch.encoder_lens,
212
+ spec_info=forward_batch.spec_info,
213
+ )
214
+ self.forward_metadata = PrefillMetadata(
215
+ self.prefill_wrappers_paged, False, False
216
+ )
217
+ elif forward_batch.forward_mode.is_target_verify():
218
+ self.indices_updater_prefill.update(
219
+ forward_batch.req_pool_indices,
220
+ forward_batch.seq_lens,
221
+ forward_batch.seq_lens_sum,
222
+ prefix_lens=None,
223
+ prefill_wrappers=self.prefill_wrappers_verify,
224
+ use_ragged=False,
225
+ encoder_lens=forward_batch.encoder_lens,
226
+ spec_info=forward_batch.spec_info,
227
+ )
228
+ self.forward_metadata = PrefillMetadata(
229
+ self.prefill_wrappers_verify, False, False
230
+ )
231
+ else:
232
+ prefix_lens = forward_batch.extend_prefix_lens
233
+
234
+ if self.is_multimodal:
235
+ use_ragged = False
236
+ extend_no_prefix = False
237
+ else:
238
+ use_ragged = True
239
+ extend_no_prefix = not any(forward_batch.extend_prefix_lens_cpu)
240
+
241
+ self.indices_updater_prefill.update(
242
+ forward_batch.req_pool_indices,
243
+ forward_batch.seq_lens,
244
+ forward_batch.seq_lens_sum,
245
+ prefix_lens,
246
+ prefill_wrappers=self.prefill_wrappers_paged,
247
+ use_ragged=use_ragged,
248
+ encoder_lens=forward_batch.encoder_lens,
249
+ spec_info=None,
250
+ )
251
+ self.forward_metadata = PrefillMetadata(
252
+ self.prefill_wrappers_paged, use_ragged, extend_no_prefix
253
+ )
254
+
255
+ def init_cuda_graph_state(
256
+ self, max_bs: int, kv_indices_buf: Optional[torch.Tensor] = None
257
+ ):
258
+ if kv_indices_buf is None:
259
+ cuda_graph_kv_indices = torch.zeros(
260
+ (max_bs * self.max_context_len,),
261
+ dtype=torch.int32,
262
+ device="cuda",
263
+ )
264
+ else:
265
+ cuda_graph_kv_indices = kv_indices_buf
266
+
267
+ self.cuda_graph_kv_indices = [cuda_graph_kv_indices] + [
268
+ cuda_graph_kv_indices.clone() for _ in range(self.num_wrappers - 1)
269
+ ]
270
+
271
+ if not self.skip_prefill:
272
+ self.cuda_graph_custom_mask = torch.zeros(
273
+ (max_bs * self.max_context_len),
274
+ dtype=torch.uint8,
275
+ device="cuda",
276
+ )
277
+ self.cuda_graph_qk_indptr = [x.clone() for x in self.kv_indptr]
278
+ self.cuda_graph_qo_indptr = [x.clone() for x in self.kv_indptr]
279
+
280
+ def init_forward_metadata_capture_cuda_graph(
281
+ self,
282
+ bs: int,
283
+ num_tokens: int,
284
+ req_pool_indices: torch.Tensor,
285
+ seq_lens: torch.Tensor,
286
+ encoder_lens: Optional[torch.Tensor],
287
+ forward_mode: ForwardMode,
288
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
289
+ ):
290
+ if forward_mode.is_decode_or_idle():
291
+ decode_wrappers = []
292
+ for i in range(self.num_wrappers):
293
+ decode_wrappers.append(
294
+ BatchDecodeWithPagedKVCacheWrapper(
295
+ self.workspace_buffer,
296
+ "NHD",
297
+ use_cuda_graph=True,
298
+ use_tensor_cores=self.decode_use_tensor_cores,
299
+ paged_kv_indptr_buffer=self.kv_indptr[i][: num_tokens + 1],
300
+ paged_kv_indices_buffer=self.cuda_graph_kv_indices[i],
301
+ paged_kv_last_page_len_buffer=self.kv_last_page_len[
302
+ :num_tokens
303
+ ],
304
+ )
305
+ )
306
+ seq_lens_sum = seq_lens.sum().item()
307
+ self.indices_updater_decode.update(
308
+ req_pool_indices,
309
+ seq_lens,
310
+ seq_lens_sum,
311
+ decode_wrappers=decode_wrappers,
312
+ encoder_lens=encoder_lens,
313
+ spec_info=spec_info,
314
+ )
315
+ self.decode_cuda_graph_metadata[bs] = decode_wrappers
316
+ self.forward_metadata = DecodeMetadata(decode_wrappers)
317
+ for i in range(self.num_wrappers):
318
+ decode_wrappers[i].begin_forward = partial(
319
+ fast_decode_plan, decode_wrappers[i]
320
+ )
321
+ elif forward_mode.is_target_verify():
322
+ prefill_wrappers = []
323
+ for i in range(self.num_wrappers):
324
+ prefill_wrappers.append(
325
+ BatchPrefillWithPagedKVCacheWrapper(
326
+ self.workspace_buffer,
327
+ "NHD",
328
+ use_cuda_graph=True,
329
+ qo_indptr_buf=self.cuda_graph_qo_indptr[i][: bs + 1],
330
+ paged_kv_indptr_buf=self.kv_indptr[i][: bs + 1],
331
+ paged_kv_indices_buf=self.cuda_graph_kv_indices[i],
332
+ paged_kv_last_page_len_buf=self.kv_last_page_len[:bs],
333
+ custom_mask_buf=self.cuda_graph_custom_mask,
334
+ mask_indptr_buf=self.cuda_graph_qk_indptr[i][: bs + 1],
335
+ )
336
+ )
337
+ seq_lens_sum = seq_lens.sum().item()
338
+ self.indices_updater_prefill.update(
339
+ req_pool_indices,
340
+ seq_lens,
341
+ seq_lens_sum,
342
+ prefix_lens=None,
343
+ prefill_wrappers=prefill_wrappers,
344
+ use_ragged=False,
345
+ encoder_lens=encoder_lens,
346
+ spec_info=spec_info,
347
+ )
348
+ self.prefill_cuda_graph_metadata[bs] = prefill_wrappers
349
+ self.forward_metadata = PrefillMetadata(prefill_wrappers, False, False)
350
+ else:
351
+ raise ValueError(f"Invalid mode: {forward_mode=}")
352
+
353
+ def init_forward_metadata_replay_cuda_graph(
354
+ self,
355
+ bs: int,
356
+ req_pool_indices: torch.Tensor,
357
+ seq_lens: torch.Tensor,
358
+ seq_lens_sum: int,
359
+ encoder_lens: Optional[torch.Tensor],
360
+ forward_mode: ForwardMode,
361
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
362
+ seq_lens_cpu: Optional[torch.Tensor],
363
+ ):
364
+ if forward_mode.is_decode_or_idle():
365
+ self.indices_updater_decode.update(
366
+ req_pool_indices[:bs],
367
+ seq_lens[:bs],
368
+ seq_lens_sum,
369
+ decode_wrappers=self.decode_cuda_graph_metadata[bs],
370
+ encoder_lens=encoder_lens[:bs] if encoder_lens is not None else None,
371
+ spec_info=spec_info,
372
+ )
373
+ elif forward_mode.is_target_verify():
374
+ self.indices_updater_prefill.update(
375
+ req_pool_indices[:bs],
376
+ seq_lens[:bs],
377
+ seq_lens_sum,
378
+ prefix_lens=None,
379
+ prefill_wrappers=self.prefill_cuda_graph_metadata[bs],
380
+ use_ragged=False,
381
+ encoder_lens=encoder_lens[:bs] if encoder_lens is not None else None,
382
+ spec_info=spec_info,
383
+ )
384
+ else:
385
+ raise ValueError("Invalid forward mode")
386
+
387
+ def get_cuda_graph_seq_len_fill_value(self):
388
+ return 0
389
+
390
+ def forward_extend(
391
+ self,
392
+ q: torch.Tensor,
393
+ k: torch.Tensor,
394
+ v: torch.Tensor,
395
+ layer: RadixAttention,
396
+ forward_batch: ForwardBatch,
397
+ save_kv_cache=True,
398
+ ):
399
+ k_scale = layer.k_scale_float if self.kv_cache_dtype_str != "auto" else None
400
+ v_scale = layer.v_scale_float if self.kv_cache_dtype_str != "auto" else None
401
+ prefill_wrapper_paged = self.forward_metadata.prefill_wrappers[
402
+ self._get_wrapper_idx(layer)
403
+ ]
404
+ cache_loc = (
405
+ forward_batch.out_cache_loc
406
+ if not layer.is_cross_attention
407
+ else forward_batch.encoder_out_cache_loc
408
+ )
409
+
410
+ logits_soft_cap = layer.logit_cap
411
+
412
+ if not self.forward_metadata.use_ragged:
413
+ if k is not None:
414
+ assert v is not None
415
+ if save_kv_cache:
416
+ forward_batch.token_to_kv_pool.set_kv_buffer(
417
+ layer, cache_loc, k, v, k_scale, v_scale
418
+ )
419
+
420
+ o = prefill_wrapper_paged.forward(
421
+ q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
422
+ forward_batch.token_to_kv_pool.get_kv_buffer(layer.layer_id),
423
+ causal=not layer.is_cross_attention,
424
+ sm_scale=layer.scaling,
425
+ window_left=layer.sliding_window_size,
426
+ logits_soft_cap=logits_soft_cap,
427
+ k_scale=k_scale,
428
+ v_scale=v_scale,
429
+ )
430
+ else:
431
+ o1, s1 = self.prefill_wrapper_ragged.forward_return_lse(
432
+ q.view(-1, layer.tp_q_head_num, layer.head_dim),
433
+ k.view(-1, layer.tp_k_head_num, layer.head_dim),
434
+ v.view(-1, layer.tp_v_head_num, layer.head_dim),
435
+ causal=True,
436
+ sm_scale=layer.scaling,
437
+ logits_soft_cap=logits_soft_cap,
438
+ )
439
+
440
+ if self.forward_metadata.extend_no_prefix:
441
+ o = o1
442
+ else:
443
+ o2, s2 = prefill_wrapper_paged.forward_return_lse(
444
+ q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
445
+ forward_batch.token_to_kv_pool.get_kv_buffer(layer.layer_id),
446
+ causal=False,
447
+ sm_scale=layer.scaling,
448
+ logits_soft_cap=logits_soft_cap,
449
+ )
450
+
451
+ o, _ = merge_state(o1, s1, o2, s2)
452
+
453
+ if save_kv_cache:
454
+ forward_batch.token_to_kv_pool.set_kv_buffer(
455
+ layer, cache_loc, k, v, k_scale, v_scale
456
+ )
457
+
458
+ return o.view(-1, layer.tp_q_head_num * layer.head_dim)
459
+
460
+ def forward_decode(
461
+ self,
462
+ q: torch.Tensor,
463
+ k: torch.Tensor,
464
+ v: torch.Tensor,
465
+ layer: RadixAttention,
466
+ forward_batch: ForwardBatch,
467
+ save_kv_cache=True,
468
+ ):
469
+ k_scale = layer.k_scale_float if self.kv_cache_dtype_str != "auto" else None
470
+ v_scale = layer.v_scale_float if self.kv_cache_dtype_str != "auto" else None
471
+ decode_wrapper = self.forward_metadata.decode_wrappers[
472
+ self._get_wrapper_idx(layer)
473
+ ]
474
+ cache_loc = (
475
+ forward_batch.out_cache_loc
476
+ if not layer.is_cross_attention
477
+ else forward_batch.encoder_out_cache_loc
478
+ )
479
+
480
+ if k is not None:
481
+ assert v is not None
482
+ if save_kv_cache:
483
+ forward_batch.token_to_kv_pool.set_kv_buffer(
484
+ layer, cache_loc, k, v, k_scale, v_scale
485
+ )
486
+
487
+ o = decode_wrapper.forward(
488
+ q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
489
+ forward_batch.token_to_kv_pool.get_kv_buffer(layer.layer_id),
490
+ sm_scale=layer.scaling,
491
+ logits_soft_cap=layer.logit_cap,
492
+ k_scale=k_scale,
493
+ v_scale=v_scale,
494
+ )
495
+
496
+ return o.view(-1, layer.tp_q_head_num * layer.head_dim)
497
+
498
+ def _get_wrapper_idx(self, layer: RadixAttention):
499
+ if self.num_wrappers == 1:
500
+ return 0
501
+
502
+ if self.dispatch_reason == WrapperDispatch.SLIDING_WINDOW:
503
+ return layer.sliding_window_size == -1
504
+ if self.dispatch_reason == WrapperDispatch.CROSS_ATTENTION:
505
+ return layer.is_cross_attention
506
+
507
+ raise ValueError(f"Unknown dispatch reason: {self.dispatch_reason}")
508
+
509
+
510
+ class FlashInferIndicesUpdaterDecode:
511
+ def __init__(self, model_runner: ModelRunner, attn_backend: AttentionBackend):
512
+ # Parse Constants
513
+ self.num_qo_heads = (
514
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
515
+ )
516
+ self.num_kv_heads = model_runner.model_config.get_num_kv_heads(
517
+ get_attention_tp_size()
518
+ )
519
+ self.head_dim = model_runner.model_config.head_dim
520
+ self.data_type = model_runner.kv_cache_dtype
521
+ self.q_data_type = model_runner.dtype
522
+ self.sliding_window_size = model_runner.sliding_window_size
523
+ self.attn_backend = attn_backend
524
+
525
+ # Buffers and wrappers
526
+ self.kv_indptr = attn_backend.kv_indptr
527
+ self.kv_last_page_len = attn_backend.kv_last_page_len
528
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
529
+
530
+ # Dispatch the update function
531
+ if self.attn_backend.dispatch_reason == WrapperDispatch.SLIDING_WINDOW:
532
+ self.update = self.update_sliding_window
533
+ elif self.attn_backend.dispatch_reason == WrapperDispatch.CROSS_ATTENTION:
534
+ self.update = self.update_cross_attention
535
+ else:
536
+ assert self.attn_backend.num_wrappers == 1
537
+ self.update = self.update_single_wrapper
538
+
539
+ def update(
540
+ self,
541
+ req_pool_indices: torch.Tensor,
542
+ seq_lens: torch.Tensor,
543
+ seq_lens_sum: int,
544
+ decode_wrappers: List[BatchDecodeWithPagedKVCacheWrapper],
545
+ encoder_lens: Optional[torch.Tensor],
546
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
547
+ ):
548
+ # Keep the signature for type checking. It will be assigned during runtime.
549
+ raise NotImplementedError()
550
+
551
+ def update_single_wrapper(
552
+ self,
553
+ req_pool_indices: torch.Tensor,
554
+ seq_lens: torch.Tensor,
555
+ seq_lens_sum: int,
556
+ decode_wrappers: List[BatchDecodeWithPagedKVCacheWrapper],
557
+ encoder_lens: Optional[torch.Tensor],
558
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
559
+ ):
560
+ decode_wrappers = decode_wrappers or self.decode_wrappers
561
+ self.call_begin_forward(
562
+ decode_wrappers[0],
563
+ req_pool_indices,
564
+ seq_lens,
565
+ seq_lens_sum,
566
+ self.kv_indptr[0],
567
+ None,
568
+ spec_info,
569
+ )
570
+
571
+ def update_sliding_window(
572
+ self,
573
+ req_pool_indices: torch.Tensor,
574
+ seq_lens: torch.Tensor,
575
+ seq_lens_sum: int,
576
+ decode_wrappers: List[BatchDecodeWithPagedKVCacheWrapper],
577
+ encoder_lens: Optional[torch.Tensor],
578
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
579
+ ):
580
+ for wrapper_id in range(2):
581
+ if wrapper_id == 0:
582
+ # Sliding window attention
583
+ paged_kernel_lens_tmp = torch.minimum( # TODO: replace this with clamp
584
+ seq_lens,
585
+ torch.tensor(self.sliding_window_size + 1),
586
+ )
587
+ paged_kernel_lens_sum_tmp = paged_kernel_lens_tmp.sum().item()
588
+ kv_start_idx_tmp = seq_lens - paged_kernel_lens_tmp
589
+ else:
590
+ # Full attention
591
+ paged_kernel_lens_tmp = seq_lens
592
+ paged_kernel_lens_sum_tmp = seq_lens_sum
593
+ kv_start_idx_tmp = None
594
+
595
+ self.call_begin_forward(
596
+ decode_wrappers[wrapper_id],
597
+ req_pool_indices,
598
+ paged_kernel_lens_tmp,
599
+ paged_kernel_lens_sum_tmp,
600
+ self.kv_indptr[wrapper_id],
601
+ kv_start_idx_tmp,
602
+ spec_info,
603
+ )
604
+
605
+ def update_cross_attention(
606
+ self,
607
+ req_pool_indices: torch.Tensor,
608
+ seq_lens: torch.Tensor,
609
+ seq_lens_sum: int,
610
+ decode_wrappers: List[BatchDecodeWithPagedKVCacheWrapper],
611
+ encoder_lens: Optional[torch.Tensor],
612
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
613
+ ):
614
+ for wrapper_id in range(2):
615
+ if wrapper_id == 0:
616
+ # Normal attention
617
+ paged_kernel_lens = seq_lens
618
+ kv_start_idx = encoder_lens
619
+ else:
620
+ # Cross attention
621
+ paged_kernel_lens = encoder_lens
622
+ kv_start_idx = torch.zeros_like(encoder_lens)
623
+ seq_lens_sum = encoder_lens.sum().item()
624
+
625
+ self.call_begin_forward(
626
+ decode_wrappers[wrapper_id],
627
+ req_pool_indices,
628
+ paged_kernel_lens,
629
+ seq_lens_sum,
630
+ self.kv_indptr[wrapper_id],
631
+ kv_start_idx,
632
+ spec_info,
633
+ )
634
+
635
+ def call_begin_forward(
636
+ self,
637
+ wrapper: BatchDecodeWithPagedKVCacheWrapper,
638
+ req_pool_indices: torch.Tensor,
639
+ paged_kernel_lens: torch.Tensor,
640
+ paged_kernel_lens_sum: int,
641
+ kv_indptr: torch.Tensor,
642
+ kv_start_idx: torch.Tensor,
643
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
644
+ ):
645
+ if spec_info is None:
646
+ bs = len(req_pool_indices)
647
+ kv_indptr[1 : bs + 1] = torch.cumsum(paged_kernel_lens, dim=0)
648
+ kv_indptr = kv_indptr[: bs + 1]
649
+
650
+ if wrapper.is_cuda_graph_enabled:
651
+ # Directly write to the cuda graph input buffer
652
+ kv_indices = wrapper._paged_kv_indices_buf
653
+ else:
654
+ kv_indices = torch.empty(
655
+ paged_kernel_lens_sum, dtype=torch.int32, device="cuda"
656
+ )
657
+
658
+ create_flashinfer_kv_indices_triton[(bs,)](
659
+ self.req_to_token,
660
+ req_pool_indices,
661
+ paged_kernel_lens,
662
+ kv_indptr,
663
+ kv_start_idx,
664
+ kv_indices,
665
+ self.req_to_token.shape[1],
666
+ )
667
+ else:
668
+ kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
669
+ bs = kv_indptr.shape[0] - 1
670
+
671
+ wrapper.begin_forward(
672
+ kv_indptr,
673
+ kv_indices,
674
+ self.kv_last_page_len[:bs],
675
+ self.num_qo_heads,
676
+ self.num_kv_heads,
677
+ self.head_dim,
678
+ 1,
679
+ data_type=self.data_type,
680
+ q_data_type=self.q_data_type,
681
+ non_blocking=True,
682
+ )
683
+
684
+
685
+ class FlashInferIndicesUpdaterPrefill:
686
+ def __init__(self, model_runner: ModelRunner, attn_backend: AttentionBackend):
687
+ # Parse Constants
688
+ self.num_qo_heads = (
689
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
690
+ )
691
+ self.num_kv_heads = model_runner.model_config.get_num_kv_heads(
692
+ get_attention_tp_size()
693
+ )
694
+ self.head_dim = model_runner.model_config.head_dim
695
+ self.data_type = model_runner.kv_cache_dtype
696
+ self.q_data_type = model_runner.dtype
697
+ self.sliding_window_size = model_runner.sliding_window_size
698
+ self.attn_backend = attn_backend
699
+
700
+ # Buffers and wrappers
701
+ self.kv_indptr = attn_backend.kv_indptr
702
+ self.kv_last_page_len = attn_backend.kv_last_page_len
703
+ self.qo_indptr = attn_backend.qo_indptr
704
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
705
+ self.prefill_wrapper_ragged = attn_backend.prefill_wrapper_ragged
706
+
707
+ # Dispatch the update function
708
+ if self.attn_backend.dispatch_reason == WrapperDispatch.SLIDING_WINDOW:
709
+ self.update = self.update_sliding_window
710
+ elif self.attn_backend.dispatch_reason == WrapperDispatch.CROSS_ATTENTION:
711
+ self.update = self.update_cross_attention
712
+ else:
713
+ assert self.attn_backend.num_wrappers == 1
714
+ self.update = self.update_single_wrapper
715
+
716
+ def update(
717
+ self,
718
+ req_pool_indices: torch.Tensor,
719
+ seq_lens: torch.Tensor,
720
+ seq_lens_sum: int,
721
+ prefix_lens: torch.Tensor,
722
+ prefill_wrappers: List[BatchPrefillWithPagedKVCacheWrapper],
723
+ use_ragged: bool,
724
+ encoder_lens: Optional[torch.Tensor],
725
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
726
+ ):
727
+ # Keep the signature for type checking. It will be assigned during runtime.
728
+ raise NotImplementedError()
729
+
730
+ def update_single_wrapper(
731
+ self,
732
+ req_pool_indices: torch.Tensor,
733
+ seq_lens: torch.Tensor,
734
+ seq_lens_sum: int,
735
+ prefix_lens: torch.Tensor,
736
+ prefill_wrappers: List[BatchPrefillWithPagedKVCacheWrapper],
737
+ use_ragged: bool,
738
+ encoder_lens: Optional[torch.Tensor],
739
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
740
+ ):
741
+ if use_ragged:
742
+ paged_kernel_lens = prefix_lens
743
+ paged_kernel_lens_sum = paged_kernel_lens.sum().item()
744
+ else:
745
+ paged_kernel_lens = seq_lens
746
+ paged_kernel_lens_sum = seq_lens_sum
747
+
748
+ self.call_begin_forward(
749
+ self.prefill_wrapper_ragged,
750
+ prefill_wrappers[0],
751
+ req_pool_indices,
752
+ paged_kernel_lens,
753
+ paged_kernel_lens_sum,
754
+ seq_lens,
755
+ prefix_lens,
756
+ None,
757
+ self.kv_indptr[0],
758
+ self.qo_indptr[0],
759
+ use_ragged,
760
+ spec_info,
761
+ )
762
+
763
+ def update_sliding_window(
764
+ self,
765
+ req_pool_indices: torch.Tensor,
766
+ seq_lens: torch.Tensor,
767
+ seq_lens_sum: int,
768
+ prefix_lens: torch.Tensor,
769
+ prefill_wrappers: List[BatchPrefillWithPagedKVCacheWrapper],
770
+ use_ragged: bool,
771
+ encoder_lens: Optional[torch.Tensor],
772
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
773
+ ):
774
+ for wrapper_id in range(2):
775
+ if wrapper_id == 0:
776
+ # window attention use paged only
777
+ paged_kernel_lens = torch.minimum(
778
+ seq_lens,
779
+ torch.tensor(self.sliding_window_size) + seq_lens - prefix_lens,
780
+ )
781
+ paged_kernel_lens_sum = paged_kernel_lens.sum().item()
782
+ else:
783
+ # full attention
784
+ paged_kernel_lens = seq_lens
785
+ paged_kernel_lens_sum = seq_lens_sum
786
+
787
+ kv_start_idx = seq_lens - paged_kernel_lens
788
+
789
+ self.call_begin_forward(
790
+ self.prefill_wrapper_ragged,
791
+ prefill_wrappers[wrapper_id],
792
+ req_pool_indices,
793
+ paged_kernel_lens,
794
+ paged_kernel_lens_sum,
795
+ seq_lens,
796
+ prefix_lens,
797
+ kv_start_idx,
798
+ self.kv_indptr[wrapper_id],
799
+ self.qo_indptr[wrapper_id],
800
+ use_ragged,
801
+ spec_info,
802
+ )
803
+
804
+ def update_cross_attention(
805
+ self,
806
+ req_pool_indices: torch.Tensor,
807
+ seq_lens: torch.Tensor,
808
+ seq_lens_sum: int,
809
+ prefix_lens: torch.Tensor,
810
+ prefill_wrappers: List[BatchPrefillWithPagedKVCacheWrapper],
811
+ use_ragged: bool,
812
+ encoder_lens: Optional[torch.Tensor],
813
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
814
+ ):
815
+ for wrapper_id in range(2):
816
+ if wrapper_id == 0:
817
+ # normal attention
818
+ paged_kernel_lens = seq_lens
819
+ kv_start_idx = encoder_lens
820
+ paged_kernel_lens_sum = seq_lens_sum
821
+ else:
822
+ # cross attention
823
+ paged_kernel_lens = encoder_lens
824
+ kv_start_idx = torch.zeros_like(encoder_lens)
825
+ paged_kernel_lens_sum = paged_kernel_lens.sum().item()
826
+
827
+ self.call_begin_forward(
828
+ self.prefill_wrapper_ragged,
829
+ prefill_wrappers[wrapper_id],
830
+ req_pool_indices,
831
+ paged_kernel_lens,
832
+ paged_kernel_lens_sum,
833
+ seq_lens,
834
+ prefix_lens,
835
+ kv_start_idx,
836
+ self.kv_indptr[wrapper_id],
837
+ self.qo_indptr[wrapper_id],
838
+ use_ragged,
839
+ spec_info,
840
+ )
841
+
842
+ def call_begin_forward(
843
+ self,
844
+ wrapper_ragged: BatchPrefillWithRaggedKVCacheWrapper,
845
+ wrapper_paged: BatchPrefillWithPagedKVCacheWrapper,
846
+ req_pool_indices: torch.Tensor,
847
+ paged_kernel_lens: torch.Tensor,
848
+ paged_kernel_lens_sum: int,
849
+ seq_lens: torch.Tensor,
850
+ prefix_lens: torch.Tensor,
851
+ kv_start_idx: torch.Tensor,
852
+ kv_indptr: torch.Tensor,
853
+ qo_indptr: torch.Tensor,
854
+ use_ragged: bool,
855
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
856
+ ):
857
+ bs = len(seq_lens)
858
+ if spec_info is None:
859
+ assert len(seq_lens) == len(req_pool_indices)
860
+ # Normal extend
861
+ kv_indptr[1 : bs + 1] = torch.cumsum(paged_kernel_lens, dim=0)
862
+ kv_indptr = kv_indptr[: bs + 1]
863
+ kv_indices = torch.empty(
864
+ paged_kernel_lens_sum + 256,
865
+ dtype=torch.int32,
866
+ device=req_pool_indices.device,
867
+ )
868
+ create_flashinfer_kv_indices_triton[(bs,)](
869
+ self.req_to_token,
870
+ req_pool_indices,
871
+ paged_kernel_lens,
872
+ kv_indptr,
873
+ kv_start_idx,
874
+ kv_indices,
875
+ self.req_to_token.shape[1],
876
+ )
877
+ qo_indptr[1 : bs + 1] = torch.cumsum(seq_lens - prefix_lens, dim=0)
878
+ qo_indptr = qo_indptr[: bs + 1]
879
+ custom_mask = None
880
+ else:
881
+ assert isinstance(spec_info, EagleDraftInput) or isinstance(
882
+ spec_info, EagleVerifyInput
883
+ )
884
+ kv_indices, kv_indptr, qo_indptr, custom_mask = (
885
+ spec_info.generate_attn_arg_prefill(
886
+ req_pool_indices,
887
+ paged_kernel_lens,
888
+ paged_kernel_lens_sum,
889
+ self.req_to_token,
890
+ )
891
+ )
892
+
893
+ # extend part
894
+ if use_ragged:
895
+ wrapper_ragged.begin_forward(
896
+ qo_indptr,
897
+ qo_indptr,
898
+ self.num_qo_heads,
899
+ self.num_kv_heads,
900
+ self.head_dim,
901
+ q_data_type=self.q_data_type,
902
+ )
903
+
904
+ # cached part
905
+ wrapper_paged.begin_forward(
906
+ qo_indptr,
907
+ kv_indptr,
908
+ kv_indices,
909
+ self.kv_last_page_len[:bs],
910
+ self.num_qo_heads,
911
+ self.num_kv_heads,
912
+ self.head_dim,
913
+ 1,
914
+ q_data_type=self.q_data_type,
915
+ kv_data_type=self.data_type,
916
+ custom_mask=custom_mask,
917
+ non_blocking=True,
918
+ )
919
+
920
+
921
+ # Use as a fast path to override the indptr in flashinfer's plan function
922
+ # This is used to remove some host-to-device copy overhead.
923
+ global global_override_indptr_cpu
924
+
925
+
926
+ class FlashInferMultiStepDraftBackend:
927
+ """
928
+ Wrap multiple flashinfer attention backends as one for multiple consecutive
929
+ draft decoding steps.
930
+ """
931
+
932
+ def __init__(
933
+ self,
934
+ model_runner: ModelRunner,
935
+ topk: int,
936
+ speculative_num_steps: int,
937
+ ):
938
+ from sglang.srt.speculative.eagle_utils import generate_draft_decode_kv_indices
939
+
940
+ self.topk = topk
941
+ self.speculative_num_steps = speculative_num_steps
942
+ self.generate_draft_decode_kv_indices = generate_draft_decode_kv_indices
943
+ self.page_size = model_runner.page_size
944
+
945
+ max_bs = model_runner.req_to_token_pool.size * self.topk
946
+ self.kv_indptr = torch.zeros(
947
+ (
948
+ self.speculative_num_steps,
949
+ max_bs + 1,
950
+ ),
951
+ dtype=torch.int32,
952
+ device=model_runner.device,
953
+ )
954
+ self.kv_last_page_len = torch.ones(
955
+ (max_bs,), dtype=torch.int32, device=model_runner.device
956
+ )
957
+ self.attn_backends = []
958
+ for i in range(self.speculative_num_steps):
959
+ self.attn_backends.append(
960
+ FlashInferAttnBackend(
961
+ model_runner,
962
+ skip_prefill=True,
963
+ kv_indptr_buf=self.kv_indptr[i],
964
+ kv_last_page_len_buf=self.kv_last_page_len,
965
+ )
966
+ )
967
+
968
+ self.max_context_len = self.attn_backends[0].max_context_len
969
+
970
+ # Cached variables for generate_draft_decode_kv_indices
971
+ self.pool_len = model_runner.req_to_token_pool.req_to_token.shape[1]
972
+
973
+ def common_template(
974
+ self,
975
+ forward_batch: ForwardBatch,
976
+ kv_indices_buffer: torch.Tensor,
977
+ call_fn: Callable,
978
+ ):
979
+ num_seqs = forward_batch.batch_size
980
+ bs = self.topk * num_seqs
981
+ seq_lens_sum = forward_batch.seq_lens_sum
982
+
983
+ self.generate_draft_decode_kv_indices[
984
+ (self.speculative_num_steps, num_seqs, self.topk)
985
+ ](
986
+ forward_batch.req_pool_indices,
987
+ forward_batch.req_to_token_pool.req_to_token,
988
+ forward_batch.seq_lens,
989
+ kv_indices_buffer,
990
+ self.kv_indptr,
991
+ forward_batch.positions,
992
+ num_seqs,
993
+ self.topk,
994
+ self.pool_len,
995
+ kv_indices_buffer.shape[1],
996
+ self.kv_indptr.shape[1],
997
+ next_power_of_2(num_seqs),
998
+ next_power_of_2(self.speculative_num_steps),
999
+ next_power_of_2(bs),
1000
+ )
1001
+
1002
+ assert forward_batch.spec_info is not None
1003
+ assert isinstance(forward_batch.spec_info, EagleDraftInput)
1004
+
1005
+ # Copy the kv_indptr once to avoid multiple device-to-host copies in flashinfer's plan.
1006
+ indptr_cpu_whole = self.kv_indptr[:, : bs + 1].cpu()
1007
+ global global_override_indptr_cpu
1008
+
1009
+ for i in range(self.speculative_num_steps - 1):
1010
+ forward_batch.spec_info.kv_indptr = self.kv_indptr[i, : bs + 1]
1011
+ forward_batch.spec_info.kv_indices = kv_indices_buffer[i][
1012
+ : seq_lens_sum * self.topk + bs * (i + 1)
1013
+ ]
1014
+ global_override_indptr_cpu = indptr_cpu_whole[i]
1015
+ call_fn(i, forward_batch)
1016
+
1017
+ global_override_indptr_cpu = None
1018
+
1019
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
1020
+ kv_indices = torch.empty(
1021
+ (
1022
+ self.speculative_num_steps,
1023
+ forward_batch.batch_size * self.topk * self.max_context_len,
1024
+ ),
1025
+ dtype=torch.int32,
1026
+ device="cuda",
1027
+ )
1028
+
1029
+ def call_fn(i, forward_batch):
1030
+ forward_batch.spec_info.kv_indptr = (
1031
+ forward_batch.spec_info.kv_indptr.clone()
1032
+ )
1033
+ forward_batch.spec_info.kv_indices = (
1034
+ forward_batch.spec_info.kv_indices.clone()
1035
+ )
1036
+ self.attn_backends[i].init_forward_metadata(forward_batch)
1037
+
1038
+ self.common_template(forward_batch, kv_indices, call_fn)
1039
+
1040
+ def init_cuda_graph_state(self, max_bs: int):
1041
+ self.cuda_graph_kv_indices = torch.zeros(
1042
+ (self.speculative_num_steps, max_bs * self.max_context_len),
1043
+ dtype=torch.int32,
1044
+ device="cuda",
1045
+ )
1046
+
1047
+ for i in range(self.speculative_num_steps):
1048
+ self.attn_backends[i].init_cuda_graph_state(
1049
+ max_bs, kv_indices_buf=self.cuda_graph_kv_indices[i]
1050
+ )
1051
+
1052
+ def init_forward_metadata_capture_cuda_graph(self, forward_batch: ForwardBatch):
1053
+ def call_fn(i, forward_batch):
1054
+ self.attn_backends[i].init_forward_metadata_capture_cuda_graph(
1055
+ forward_batch.batch_size,
1056
+ forward_batch.batch_size * self.topk,
1057
+ forward_batch.req_pool_indices,
1058
+ forward_batch.seq_lens,
1059
+ encoder_lens=None,
1060
+ forward_mode=ForwardMode.DECODE,
1061
+ spec_info=forward_batch.spec_info,
1062
+ )
1063
+
1064
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
1065
+
1066
+ def init_forward_metadata_replay_cuda_graph(
1067
+ self, forward_batch: ForwardBatch, bs: int
1068
+ ):
1069
+ def call_fn(i, forward_batch):
1070
+ self.attn_backends[i].init_forward_metadata_replay_cuda_graph(
1071
+ bs,
1072
+ forward_batch.req_pool_indices,
1073
+ forward_batch.seq_lens,
1074
+ seq_lens_sum=-1,
1075
+ encoder_lens=None,
1076
+ forward_mode=ForwardMode.DECODE,
1077
+ spec_info=forward_batch.spec_info,
1078
+ seq_lens_cpu=None,
1079
+ )
1080
+
1081
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
1082
+
1083
+
1084
+ def should_use_tensor_core(
1085
+ kv_cache_dtype: torch.dtype,
1086
+ num_attention_heads: int,
1087
+ num_kv_heads: int,
1088
+ ) -> bool:
1089
+ """
1090
+ Determine whether to use tensor cores for attention computation.
1091
+
1092
+ Args:
1093
+ kv_cache_dtype: Data type of the KV cache
1094
+ num_attention_heads: Number of attention heads
1095
+ num_kv_heads: Number of key/value heads
1096
+
1097
+ Returns:
1098
+ bool: Whether to use tensor cores
1099
+ """
1100
+ # Try to use environment variable first
1101
+ env_override = os.environ.get("SGLANG_FLASHINFER_USE_TENSOR_CORE")
1102
+ if env_override is not None:
1103
+ return env_override.lower() == "true"
1104
+
1105
+ # Try to use _grouped_size_compiled_for_decode_kernels if available
1106
+ # This is for flashinfer <=0.1.6. Otherwise, there is an accuracy bug
1107
+ try:
1108
+ from flashinfer.decode import _grouped_size_compiled_for_decode_kernels
1109
+
1110
+ if not _grouped_size_compiled_for_decode_kernels(
1111
+ num_attention_heads,
1112
+ num_kv_heads,
1113
+ ):
1114
+ return True
1115
+ else:
1116
+ return False
1117
+ except (ImportError, AttributeError):
1118
+ pass
1119
+
1120
+ # Calculate GQA group size
1121
+ gqa_group_size = num_attention_heads // num_kv_heads
1122
+
1123
+ # Determine based on dtype and GQA group size
1124
+ if kv_cache_dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
1125
+ return True
1126
+ elif kv_cache_dtype in (torch.float16, torch.half, torch.bfloat16):
1127
+ return gqa_group_size > 4
1128
+ else:
1129
+ return False
1130
+
1131
+
1132
+ # Use as a fast path to override the indptr in flashinfer's plan function
1133
+ # This is used to remove some host-to-device copy overhead.
1134
+ global_override_indptr_cpu = None
1135
+
1136
+
1137
+ def fast_decode_plan(
1138
+ self,
1139
+ indptr: torch.Tensor,
1140
+ indices: torch.Tensor,
1141
+ last_page_len: torch.Tensor,
1142
+ num_qo_heads: int,
1143
+ num_kv_heads: int,
1144
+ head_dim: int,
1145
+ page_size: int,
1146
+ pos_encoding_mode: str = "NONE",
1147
+ window_left: int = -1,
1148
+ logits_soft_cap: Optional[float] = None,
1149
+ data_type: Union[str, torch.dtype] = "float16",
1150
+ q_data_type: Optional[Union[str, torch.dtype]] = None,
1151
+ sm_scale: Optional[float] = None,
1152
+ rope_scale: Optional[float] = None,
1153
+ rope_theta: Optional[float] = None,
1154
+ non_blocking: bool = True,
1155
+ ) -> None:
1156
+ """
1157
+ A faster version of BatchDecodeWithPagedKVCacheWrapper::plan used for FlashInferMultiStepDraftBackend.
1158
+ Modifications:
1159
+ - Remove unnecessary device-to-device copy for the cuda graph buffers.
1160
+ - Remove unnecessary host-to-device copy for the metadata buffers.
1161
+ """
1162
+ batch_size = len(last_page_len)
1163
+ if logits_soft_cap is None:
1164
+ logits_soft_cap = 0.0
1165
+
1166
+ if self.use_tensor_cores:
1167
+ qo_indptr_host = _get_range_buf(batch_size + 1, "cpu")
1168
+
1169
+ if self.is_cuda_graph_enabled:
1170
+ if batch_size != self._fixed_batch_size:
1171
+ raise ValueError(
1172
+ "The batch size should be fixed in cudagraph mode, the runtime batch size {} "
1173
+ " mismatches the batch size set during initialization {}".format(
1174
+ batch_size, self._fixed_batch_size
1175
+ )
1176
+ )
1177
+ if len(indices) > len(self._paged_kv_indices_buf):
1178
+ raise ValueError(
1179
+ "The size of indices should be less than or equal to the allocated buffer"
1180
+ )
1181
+ # Skip these copies because we directly write to them during prepartion
1182
+ # self._paged_kv_indptr_buf.copy_(indptr)
1183
+ # self._paged_kv_indices_buf[: len(indices)] = indices
1184
+ # self._paged_kv_last_page_len_buf.copy_(last_page_len)
1185
+ else:
1186
+ self._paged_kv_indptr_buf = indptr
1187
+ self._paged_kv_indices_buf = indices
1188
+ self._paged_kv_last_page_len_buf = last_page_len
1189
+ self._qo_indptr_buf = qo_indptr_host.to(self.device, non_blocking=non_blocking)
1190
+
1191
+ # NOTE(Zihao): the following tensors acts as placeholder to pass dtype info
1192
+ if not q_data_type:
1193
+ q_data_type = data_type
1194
+
1195
+ if not hasattr(self, "empty_q_data"):
1196
+ self.empty_q_data = torch.empty(
1197
+ 0,
1198
+ dtype=(
1199
+ getattr(torch, q_data_type)
1200
+ if isinstance(q_data_type, str)
1201
+ else q_data_type
1202
+ ),
1203
+ )
1204
+ self.empty_kv_cache = torch.empty(
1205
+ 0,
1206
+ dtype=(
1207
+ getattr(torch, data_type) if isinstance(data_type, str) else data_type
1208
+ ),
1209
+ )
1210
+ self.last_page_len = torch.ones(32768, dtype=torch.int32)
1211
+
1212
+ indptr_host = (
1213
+ global_override_indptr_cpu
1214
+ if global_override_indptr_cpu is not None
1215
+ else indptr.cpu()
1216
+ )
1217
+
1218
+ if self.use_tensor_cores:
1219
+ kv_lens_arr_host = get_seq_lens(
1220
+ indptr_host, self.last_page_len[:batch_size], page_size
1221
+ )
1222
+
1223
+ self._plan_info = self._cached_module.plan(
1224
+ self._float_workspace_buffer,
1225
+ self._int_workspace_buffer,
1226
+ self._pin_memory_int_workspace_buffer,
1227
+ qo_indptr_host,
1228
+ indptr_host,
1229
+ kv_lens_arr_host,
1230
+ batch_size, # total_num_rows
1231
+ batch_size,
1232
+ num_qo_heads,
1233
+ num_kv_heads,
1234
+ page_size,
1235
+ self.is_cuda_graph_enabled,
1236
+ head_dim,
1237
+ head_dim,
1238
+ False, # causal
1239
+ torch.cuda.current_stream().cuda_stream,
1240
+ )
1241
+ else:
1242
+ self._plan_info = self._cached_module.plan(
1243
+ self._float_workspace_buffer,
1244
+ self._int_workspace_buffer,
1245
+ self._pin_memory_int_workspace_buffer,
1246
+ indptr_host,
1247
+ batch_size,
1248
+ num_qo_heads,
1249
+ num_kv_heads,
1250
+ page_size,
1251
+ self.is_cuda_graph_enabled,
1252
+ window_left,
1253
+ logits_soft_cap,
1254
+ head_dim,
1255
+ head_dim,
1256
+ self.empty_q_data,
1257
+ self.empty_kv_cache,
1258
+ torch.cuda.current_stream().cuda_stream,
1259
+ )
1260
+
1261
+ self._pos_encoding_mode = pos_encoding_mode
1262
+ self._window_left = window_left
1263
+ self._logits_soft_cap = logits_soft_cap
1264
+ self._sm_scale = sm_scale
1265
+ self._rope_scale = rope_scale
1266
+ self._rope_theta = rope_theta
lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_mla_backend.py ADDED
@@ -0,0 +1,840 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Support attention backend for flashinfer MLA.
5
+ The flashinfer_mla_disable_ragged flag controls whether to use ragged prefill wrapper and defaults to be false.
6
+ When it's set to false, all wrappers are BatchMLAPaged wrapper.
7
+ When it's set to true, the backend uses BatchRagged and BatchMLAPaged wrapper for prefilling,
8
+ and uses BatchMLAPaged wrapper for decoding.
9
+ More details can be found in https://docs.flashinfer.ai/api/mla.html
10
+ """
11
+
12
+ from dataclasses import dataclass
13
+ from functools import partial
14
+ from typing import TYPE_CHECKING, Callable, Optional, Union
15
+
16
+ import torch
17
+ import triton
18
+
19
+ from sglang.global_config import global_config
20
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
21
+ from sglang.srt.layers.attention.flashinfer_backend import (
22
+ create_flashinfer_kv_indices_triton,
23
+ )
24
+ from sglang.srt.layers.dp_attention import get_attention_tp_size
25
+ from sglang.srt.managers.schedule_batch import global_server_args_dict
26
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
27
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
28
+ from sglang.srt.utils import is_flashinfer_available
29
+
30
+ if TYPE_CHECKING:
31
+ from sglang.srt.layers.radix_attention import RadixAttention
32
+ from sglang.srt.model_executor.model_runner import ModelRunner
33
+ from sglang.srt.speculative.spec_info import SpecInfo
34
+
35
+ if is_flashinfer_available():
36
+ from flashinfer import (
37
+ BatchMLAPagedAttentionWrapper,
38
+ BatchPrefillWithRaggedKVCacheWrapper,
39
+ )
40
+
41
+
42
+ @dataclass
43
+ class DecodeMetadata:
44
+ decode_wrapper: BatchMLAPagedAttentionWrapper
45
+
46
+
47
+ @dataclass
48
+ class PrefillMetadata:
49
+ prefill_wrapper: BatchMLAPagedAttentionWrapper
50
+ use_ragged: bool
51
+
52
+
53
+ # Reuse this workspace buffer across all flashinfer wrappers
54
+ global_workspace_buffer = None
55
+
56
+
57
+ class FlashInferMLAAttnBackend(AttentionBackend):
58
+ """Flashinfer attention kernels."""
59
+
60
+ def __init__(
61
+ self,
62
+ model_runner: ModelRunner,
63
+ skip_prefill: bool = False,
64
+ kv_indptr_buf: Optional[torch.Tensor] = None,
65
+ q_indptr_decode_buf: Optional[torch.Tensor] = None,
66
+ ):
67
+ super().__init__()
68
+
69
+ # Parse constants
70
+ self.max_context_len = model_runner.model_config.context_len
71
+ self.device = model_runner.device
72
+ self.skip_prefill = skip_prefill
73
+
74
+ # Allocate buffers
75
+ global global_workspace_buffer
76
+ if global_workspace_buffer is None:
77
+ global_workspace_buffer = torch.empty(
78
+ global_config.flashinfer_workspace_size,
79
+ dtype=torch.uint8,
80
+ device=model_runner.device,
81
+ )
82
+ self.workspace_buffer = global_workspace_buffer
83
+
84
+ max_bs = model_runner.req_to_token_pool.size
85
+ if kv_indptr_buf is None:
86
+ self.kv_indptr = torch.zeros(
87
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
88
+ )
89
+ else:
90
+ self.kv_indptr = kv_indptr_buf
91
+
92
+ if not self.skip_prefill:
93
+ self.qo_indptr = torch.zeros(
94
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
95
+ )
96
+
97
+ if q_indptr_decode_buf is None:
98
+ self.q_indptr_decode = torch.arange(
99
+ 0, max_bs + 1, dtype=torch.int32, device=model_runner.device
100
+ )
101
+ else:
102
+ self.q_indptr_decode = q_indptr_decode_buf
103
+
104
+ self.prefill_wrapper_ragged = BatchPrefillWithRaggedKVCacheWrapper(
105
+ self.workspace_buffer, "NHD"
106
+ )
107
+
108
+ if not self.skip_prefill:
109
+ self.prefill_wrapper_paged = BatchMLAPagedAttentionWrapper(
110
+ self.workspace_buffer,
111
+ backend="auto",
112
+ )
113
+
114
+ # FlashinferMLA backend uses mla wrapper for target verify
115
+ self.prefill_wrapper_verify = BatchMLAPagedAttentionWrapper(
116
+ self.workspace_buffer,
117
+ backend="auto",
118
+ )
119
+
120
+ self.decode_wrapper = BatchMLAPagedAttentionWrapper(
121
+ self.workspace_buffer, backend="auto"
122
+ )
123
+
124
+ # Create indices updater
125
+ if not skip_prefill:
126
+ self.indices_updater_prefill = FlashInferMLAIndicesUpdaterPrefill(
127
+ model_runner, self
128
+ )
129
+
130
+ self.indices_updater_decode = FlashInferMLAIndicesUpdaterDecode(
131
+ model_runner, self
132
+ )
133
+
134
+ # Other metadata
135
+ self.forward_metadata: Union[PrefillMetadata, DecodeMetadata] = None
136
+ self.decode_cuda_graph_metadata = {}
137
+ self.prefill_cuda_graph_metadata = {} # For verify
138
+
139
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
140
+ if forward_batch.forward_mode.is_decode_or_idle():
141
+ self.indices_updater_decode.update(
142
+ forward_batch.req_pool_indices,
143
+ forward_batch.seq_lens,
144
+ forward_batch.seq_lens_sum,
145
+ decode_wrapper=self.decode_wrapper,
146
+ init_metadata_replay=False,
147
+ )
148
+ self.forward_metadata = DecodeMetadata(self.decode_wrapper)
149
+ elif forward_batch.forward_mode.is_draft_extend():
150
+ self.indices_updater_prefill.update(
151
+ forward_batch.req_pool_indices,
152
+ forward_batch.seq_lens,
153
+ forward_batch.seq_lens_sum,
154
+ prefix_lens=None,
155
+ prefill_wrapper_paged=self.prefill_wrapper_paged,
156
+ use_ragged=False,
157
+ spec_info=forward_batch.spec_info,
158
+ )
159
+ self.forward_metadata = PrefillMetadata(self.prefill_wrapper_paged, False)
160
+ elif forward_batch.forward_mode.is_target_verify():
161
+ self.indices_updater_prefill.update(
162
+ forward_batch.req_pool_indices,
163
+ forward_batch.seq_lens,
164
+ forward_batch.seq_lens_sum,
165
+ prefix_lens=None,
166
+ prefill_wrapper_paged=self.prefill_wrapper_verify,
167
+ use_ragged=False,
168
+ spec_info=forward_batch.spec_info,
169
+ )
170
+ self.forward_metadata = PrefillMetadata(self.prefill_wrapper_verify, False)
171
+ else:
172
+ prefix_lens = forward_batch.extend_prefix_lens
173
+ extend_no_prefix = not any(forward_batch.extend_prefix_lens_cpu)
174
+ use_ragged = (
175
+ not global_server_args_dict["flashinfer_mla_disable_ragged"]
176
+ and extend_no_prefix
177
+ )
178
+
179
+ self.indices_updater_prefill.update(
180
+ forward_batch.req_pool_indices,
181
+ forward_batch.seq_lens,
182
+ forward_batch.seq_lens_sum,
183
+ prefix_lens,
184
+ prefill_wrapper_paged=self.prefill_wrapper_paged,
185
+ use_ragged=use_ragged,
186
+ )
187
+ self.forward_metadata = PrefillMetadata(
188
+ self.prefill_wrapper_paged, use_ragged
189
+ )
190
+
191
+ def init_cuda_graph_state(
192
+ self, max_bs: int, kv_indices_buf: Optional[torch.Tensor] = None
193
+ ):
194
+ if kv_indices_buf is None:
195
+ cuda_graph_kv_indices = torch.zeros(
196
+ (max_bs * self.max_context_len,),
197
+ dtype=torch.int32,
198
+ device="cuda",
199
+ )
200
+ else:
201
+ cuda_graph_kv_indices = kv_indices_buf
202
+
203
+ self.cuda_graph_kv_indices = cuda_graph_kv_indices
204
+ self.cuda_graph_qo_indptr = self.q_indptr_decode.clone()
205
+ self.cuda_graph_kv_indptr = self.kv_indptr.clone()
206
+ self.cuda_graph_kv_lens = torch.ones(
207
+ (max_bs,), dtype=torch.int32, device=self.device
208
+ )
209
+
210
+ # For fast decode plan in graph replaying
211
+ self.cuda_graph_qo_indptr_cpu = self.cuda_graph_qo_indptr.to("cpu")
212
+ self.cuda_graph_kv_indptr_cpu = self.cuda_graph_kv_indptr.to("cpu")
213
+ self.fast_decode_kwargs = {
214
+ "qo_indptr_cpu": self.cuda_graph_qo_indptr_cpu,
215
+ "kv_indptr_cpu": self.cuda_graph_kv_indptr_cpu,
216
+ "kv_indices": self.cuda_graph_kv_indices,
217
+ }
218
+
219
+ def init_forward_metadata_capture_cuda_graph(
220
+ self,
221
+ bs: int,
222
+ num_tokens: int,
223
+ req_pool_indices: torch.Tensor,
224
+ seq_lens: torch.Tensor,
225
+ encoder_lens: Optional[torch.Tensor],
226
+ forward_mode: ForwardMode,
227
+ spec_info: Optional[SpecInfo],
228
+ ):
229
+ if forward_mode.is_decode_or_idle():
230
+ decode_wrapper = BatchMLAPagedAttentionWrapper(
231
+ self.workspace_buffer,
232
+ use_cuda_graph=True,
233
+ qo_indptr=self.cuda_graph_qo_indptr[: num_tokens + 1],
234
+ kv_indptr=self.cuda_graph_kv_indptr[: num_tokens + 1],
235
+ kv_indices=self.cuda_graph_kv_indices,
236
+ kv_len_arr=self.cuda_graph_kv_lens[:num_tokens],
237
+ backend="auto",
238
+ )
239
+
240
+ seq_lens_sum = seq_lens.sum().item()
241
+ self.indices_updater_decode.update(
242
+ req_pool_indices,
243
+ seq_lens,
244
+ seq_lens_sum,
245
+ decode_wrapper=decode_wrapper,
246
+ init_metadata_replay=False,
247
+ spec_info=spec_info,
248
+ )
249
+ self.decode_cuda_graph_metadata[bs] = decode_wrapper
250
+ self.forward_metadata = DecodeMetadata(decode_wrapper)
251
+ decode_wrapper.plan = partial(fast_mla_decode_plan, decode_wrapper)
252
+ elif forward_mode.is_target_verify():
253
+ verify_wrapper = BatchMLAPagedAttentionWrapper(
254
+ self.workspace_buffer,
255
+ use_cuda_graph=True,
256
+ qo_indptr=self.cuda_graph_qo_indptr[: bs + 1],
257
+ kv_indptr=self.cuda_graph_kv_indptr[: bs + 1],
258
+ kv_indices=self.cuda_graph_kv_indices,
259
+ kv_len_arr=self.cuda_graph_kv_lens[:bs],
260
+ backend="auto",
261
+ )
262
+ seq_lens_sum = seq_lens.sum().item()
263
+ self.indices_updater_prefill.update(
264
+ req_pool_indices,
265
+ seq_lens,
266
+ seq_lens_sum,
267
+ prefix_lens=None,
268
+ prefill_wrapper_paged=verify_wrapper,
269
+ use_ragged=False,
270
+ spec_info=spec_info,
271
+ )
272
+ self.prefill_cuda_graph_metadata[bs] = verify_wrapper
273
+ self.forward_metadata = PrefillMetadata(verify_wrapper, False)
274
+ else:
275
+ raise ValueError(f"Invalid mode: {forward_mode=}")
276
+
277
+ def init_forward_metadata_replay_cuda_graph(
278
+ self,
279
+ bs: int,
280
+ req_pool_indices: torch.Tensor,
281
+ seq_lens: torch.Tensor,
282
+ seq_lens_sum: int,
283
+ encoder_lens: Optional[torch.Tensor],
284
+ forward_mode: ForwardMode,
285
+ spec_info: Optional[SpecInfo],
286
+ seq_lens_cpu: Optional[torch.Tensor],
287
+ ):
288
+ if forward_mode.is_decode_or_idle():
289
+ assert seq_lens_cpu is not None
290
+ kv_len_arr_cpu = seq_lens_cpu[:bs]
291
+ self.cuda_graph_kv_indptr_cpu[1 : bs + 1] = torch.cumsum(
292
+ kv_len_arr_cpu, dim=0
293
+ )
294
+ self.fast_decode_kwargs.update(
295
+ {
296
+ "qo_indptr_cpu": self.cuda_graph_qo_indptr_cpu[: bs + 1],
297
+ "kv_indptr_cpu": self.cuda_graph_kv_indptr_cpu[: bs + 1],
298
+ "kv_len_arr_cpu": kv_len_arr_cpu,
299
+ }
300
+ )
301
+
302
+ self.indices_updater_decode.update(
303
+ req_pool_indices[:bs],
304
+ seq_lens[:bs],
305
+ seq_lens_sum,
306
+ decode_wrapper=self.decode_cuda_graph_metadata[bs],
307
+ init_metadata_replay=True,
308
+ spec_info=spec_info,
309
+ **self.fast_decode_kwargs,
310
+ )
311
+ elif forward_mode.is_target_verify():
312
+ self.indices_updater_prefill.update(
313
+ req_pool_indices[:bs],
314
+ seq_lens[:bs],
315
+ seq_lens_sum,
316
+ prefix_lens=None,
317
+ prefill_wrapper_paged=self.prefill_cuda_graph_metadata[bs],
318
+ use_ragged=False,
319
+ spec_info=spec_info,
320
+ )
321
+ else:
322
+ raise ValueError(f"Invalid forward mode: {forward_mode=}")
323
+
324
+ def get_cuda_graph_seq_len_fill_value(self):
325
+ return 0
326
+
327
+ def forward_extend(
328
+ self,
329
+ q: torch.Tensor,
330
+ k: torch.Tensor,
331
+ v: torch.Tensor,
332
+ layer: RadixAttention,
333
+ forward_batch: ForwardBatch,
334
+ save_kv_cache: bool = True,
335
+ ):
336
+
337
+ cache_loc = forward_batch.out_cache_loc
338
+ logits_soft_cap = layer.logit_cap
339
+ prefill_wrapper_paged = self.forward_metadata.prefill_wrapper
340
+ qall = q.view(-1, layer.tp_q_head_num, layer.head_dim)
341
+ k_buf = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
342
+
343
+ # Save kv cache
344
+ if save_kv_cache and k is not None:
345
+ assert v is not None
346
+ if save_kv_cache:
347
+ forward_batch.token_to_kv_pool.set_kv_buffer(layer, cache_loc, k, v)
348
+
349
+ if self.forward_metadata.use_ragged:
350
+ # ragged prefill
351
+ o, _ = self.prefill_wrapper_ragged.forward_return_lse(
352
+ qall,
353
+ k.view(-1, layer.tp_k_head_num, layer.head_dim),
354
+ v.view(-1, layer.tp_k_head_num, layer.v_head_dim),
355
+ causal=True,
356
+ sm_scale=layer.scaling,
357
+ logits_soft_cap=logits_soft_cap,
358
+ )
359
+ else:
360
+ # mla paged prefill
361
+ o = prefill_wrapper_paged.run(
362
+ qall[:, :, : layer.v_head_dim],
363
+ qall[:, :, layer.v_head_dim :],
364
+ k_buf[:, :, : layer.v_head_dim],
365
+ k_buf[:, :, layer.v_head_dim :],
366
+ )
367
+
368
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
369
+
370
+ def forward_decode(
371
+ self,
372
+ q: torch.Tensor,
373
+ k: torch.Tensor,
374
+ v: torch.Tensor,
375
+ layer: RadixAttention,
376
+ forward_batch: ForwardBatch,
377
+ save_kv_cache: bool = True,
378
+ ):
379
+ decode_wrapper = self.forward_metadata.decode_wrapper
380
+ cache_loc = forward_batch.out_cache_loc
381
+
382
+ if k is not None:
383
+ assert v is not None
384
+ if save_kv_cache:
385
+ forward_batch.token_to_kv_pool.set_kv_buffer(
386
+ layer,
387
+ cache_loc,
388
+ k,
389
+ v,
390
+ )
391
+ reshaped_q = q.view(-1, layer.tp_q_head_num, layer.head_dim)
392
+ k_buffer = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
393
+ reshaped_k = k_buffer.view(-1, 1, layer.head_dim)
394
+ o = decode_wrapper.run(
395
+ reshaped_q[:, :, : layer.v_head_dim],
396
+ reshaped_q[:, :, layer.v_head_dim :],
397
+ reshaped_k[:, :, : layer.v_head_dim],
398
+ reshaped_k[:, :, layer.v_head_dim :],
399
+ )
400
+
401
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
402
+
403
+
404
+ class FlashInferMLAIndicesUpdaterDecode:
405
+ def __init__(self, model_runner: ModelRunner, attn_backend: AttentionBackend):
406
+ # Parse Constants
407
+ self.num_local_heads = (
408
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
409
+ )
410
+ self.kv_lora_rank = model_runner.model_config.kv_lora_rank
411
+ self.qk_nope_head_dim = model_runner.model_config.qk_nope_head_dim
412
+ self.qk_rope_head_dim = model_runner.model_config.qk_rope_head_dim
413
+ self.scaling = model_runner.model_config.scaling
414
+ self.data_type = model_runner.kv_cache_dtype
415
+ self.attn_backend = attn_backend
416
+
417
+ # Buffers and wrappers
418
+ self.kv_indptr = attn_backend.kv_indptr
419
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
420
+ self.q_indptr = attn_backend.q_indptr_decode
421
+
422
+ def update(
423
+ self,
424
+ req_pool_indices: torch.Tensor,
425
+ seq_lens: torch.Tensor,
426
+ seq_lens_sum: int,
427
+ decode_wrapper: BatchMLAPagedAttentionWrapper,
428
+ init_metadata_replay: bool = False,
429
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]] = None,
430
+ **fast_decode_kwargs,
431
+ ):
432
+ decode_wrapper = decode_wrapper or self.decode_wrapper
433
+ self.call_begin_forward(
434
+ decode_wrapper,
435
+ req_pool_indices,
436
+ seq_lens,
437
+ seq_lens_sum,
438
+ self.q_indptr,
439
+ self.kv_indptr,
440
+ init_metadata_replay,
441
+ spec_info,
442
+ **fast_decode_kwargs,
443
+ )
444
+
445
+ def call_begin_forward(
446
+ self,
447
+ wrapper: BatchMLAPagedAttentionWrapper,
448
+ req_pool_indices: torch.Tensor,
449
+ paged_kernel_lens: torch.Tensor,
450
+ paged_kernel_lens_sum: int,
451
+ q_indptr: torch.Tensor,
452
+ kv_indptr: torch.Tensor,
453
+ init_metadata_replay: bool = False,
454
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]] = None,
455
+ **fast_decode_kwargs,
456
+ ):
457
+ bs = len(req_pool_indices)
458
+ q_indptr = q_indptr[: bs + 1]
459
+ kv_lens = paged_kernel_lens.to(torch.int32)
460
+ sm_scale = self.scaling
461
+ if spec_info is None:
462
+ kv_indptr[1 : bs + 1] = torch.cumsum(paged_kernel_lens, dim=0)
463
+ kv_indptr = kv_indptr[: bs + 1]
464
+ kv_indices = (
465
+ torch.empty(paged_kernel_lens_sum, dtype=torch.int32, device="cuda")
466
+ if not init_metadata_replay
467
+ else fast_decode_kwargs["kv_indices"]
468
+ )
469
+ create_flashinfer_kv_indices_triton[(bs,)](
470
+ self.req_to_token,
471
+ req_pool_indices,
472
+ paged_kernel_lens,
473
+ kv_indptr,
474
+ None,
475
+ kv_indices,
476
+ self.req_to_token.shape[1],
477
+ )
478
+ else:
479
+ kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
480
+
481
+ if not init_metadata_replay:
482
+ wrapper.plan(
483
+ q_indptr,
484
+ kv_indptr,
485
+ kv_indices,
486
+ kv_lens,
487
+ self.num_local_heads,
488
+ self.kv_lora_rank,
489
+ self.qk_rope_head_dim,
490
+ 1,
491
+ False,
492
+ sm_scale,
493
+ self.data_type,
494
+ self.data_type,
495
+ )
496
+ else:
497
+ wrapper.plan(
498
+ fast_decode_kwargs["qo_indptr_cpu"],
499
+ fast_decode_kwargs["kv_indptr_cpu"],
500
+ kv_indices,
501
+ fast_decode_kwargs["kv_len_arr_cpu"],
502
+ self.num_local_heads,
503
+ self.kv_lora_rank,
504
+ self.qk_rope_head_dim,
505
+ 1,
506
+ False,
507
+ sm_scale,
508
+ self.data_type,
509
+ self.data_type,
510
+ )
511
+
512
+
513
+ class FlashInferMLAIndicesUpdaterPrefill:
514
+ def __init__(self, model_runner: ModelRunner, attn_backend: AttentionBackend):
515
+ # Parse Constants
516
+ self.num_local_heads = (
517
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
518
+ )
519
+ self.kv_lora_rank = model_runner.model_config.kv_lora_rank
520
+ self.qk_nope_head_dim = model_runner.model_config.qk_nope_head_dim
521
+ self.qk_rope_head_dim = model_runner.model_config.qk_rope_head_dim
522
+ self.v_head_dim = model_runner.model_config.v_head_dim
523
+ self.scaling = model_runner.model_config.scaling
524
+ self.data_type = model_runner.kv_cache_dtype
525
+ self.q_data_type = model_runner.dtype
526
+ self.attn_backend = attn_backend
527
+
528
+ # Buffers and wrappers
529
+ self.kv_indptr = attn_backend.kv_indptr
530
+ self.qo_indptr = attn_backend.qo_indptr
531
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
532
+ self.prefill_wrapper_ragged = attn_backend.prefill_wrapper_ragged
533
+
534
+ def update(
535
+ self,
536
+ req_pool_indices: torch.Tnesor,
537
+ seq_lens: torch.Tensor,
538
+ seq_lens_sum: int,
539
+ prefix_lens: torch.Tensor,
540
+ prefill_wrapper_paged: BatchMLAPagedAttentionWrapper,
541
+ use_ragged: bool,
542
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]] = None,
543
+ ):
544
+ if use_ragged:
545
+ paged_kernel_lens = prefix_lens
546
+ paged_kernel_lens_sum = paged_kernel_lens.sum().item()
547
+ else:
548
+ paged_kernel_lens = seq_lens
549
+ paged_kernel_lens_sum = seq_lens_sum
550
+
551
+ self.call_begin_forward(
552
+ self.prefill_wrapper_ragged,
553
+ prefill_wrapper_paged,
554
+ req_pool_indices,
555
+ paged_kernel_lens,
556
+ paged_kernel_lens_sum,
557
+ seq_lens,
558
+ prefix_lens,
559
+ self.kv_indptr,
560
+ self.qo_indptr,
561
+ use_ragged,
562
+ spec_info,
563
+ )
564
+
565
+ def call_begin_forward(
566
+ self,
567
+ wrapper_ragged: BatchPrefillWithRaggedKVCacheWrapper,
568
+ wrapper_paged: BatchMLAPagedAttentionWrapper,
569
+ req_pool_indices: torch.Tensor,
570
+ paged_kernel_lens: torch.Tensor,
571
+ paged_kernel_lens_sum: int,
572
+ seq_lens: torch.Tensor,
573
+ prefix_lens: torch.Tensor,
574
+ kv_indptr: torch.Tensor,
575
+ qo_indptr: torch.Tensor,
576
+ use_ragged: bool,
577
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]] = None,
578
+ ):
579
+ bs = len(seq_lens)
580
+ sm_scale = self.scaling
581
+
582
+ if spec_info is None:
583
+ assert len(seq_lens) == len(req_pool_indices)
584
+ kv_indptr[1 : bs + 1] = torch.cumsum(paged_kernel_lens, dim=0)
585
+ kv_indptr = kv_indptr[: bs + 1]
586
+ kv_indices = torch.empty(
587
+ paged_kernel_lens_sum,
588
+ dtype=torch.int32,
589
+ device=req_pool_indices.device,
590
+ )
591
+ create_flashinfer_kv_indices_triton[(bs,)](
592
+ self.req_to_token,
593
+ req_pool_indices,
594
+ paged_kernel_lens,
595
+ kv_indptr,
596
+ None,
597
+ kv_indices,
598
+ self.req_to_token.shape[1],
599
+ )
600
+ qo_indptr[1 : bs + 1] = torch.cumsum(seq_lens - prefix_lens, dim=0)
601
+ qo_indptr = qo_indptr[: bs + 1]
602
+ custom_mask = None
603
+ else:
604
+ assert isinstance(spec_info, EagleDraftInput) or isinstance(
605
+ spec_info, EagleVerifyInput
606
+ )
607
+ # TODO: Support topk > 1 with custom mask
608
+ kv_indices, kv_indptr, qo_indptr, custom_mask = (
609
+ spec_info.generate_attn_arg_prefill(
610
+ req_pool_indices,
611
+ paged_kernel_lens,
612
+ paged_kernel_lens_sum,
613
+ self.req_to_token,
614
+ )
615
+ )
616
+
617
+ if use_ragged:
618
+ # ragged prefill
619
+ wrapper_ragged.begin_forward(
620
+ qo_indptr=qo_indptr,
621
+ kv_indptr=qo_indptr,
622
+ num_qo_heads=self.num_local_heads,
623
+ num_kv_heads=self.num_local_heads,
624
+ head_dim_qk=self.qk_nope_head_dim + self.qk_rope_head_dim,
625
+ head_dim_vo=self.v_head_dim,
626
+ q_data_type=self.q_data_type,
627
+ )
628
+ else:
629
+ # mla paged prefill
630
+ kv_len_arr = kv_indptr[1:] - kv_indptr[:-1]
631
+ wrapper_paged.plan(
632
+ qo_indptr,
633
+ kv_indptr,
634
+ kv_indices,
635
+ kv_len_arr,
636
+ self.num_local_heads,
637
+ self.kv_lora_rank,
638
+ self.qk_rope_head_dim,
639
+ 1,
640
+ True,
641
+ sm_scale,
642
+ self.q_data_type,
643
+ self.data_type,
644
+ )
645
+
646
+
647
+ class FlashInferMLAMultiStepDraftBackend:
648
+ """
649
+ Wrap multiple flashinfer mla attention backends as one for multiple consecutive
650
+ draft decoding steps.
651
+ """
652
+
653
+ def __init__(
654
+ self,
655
+ model_runner: ModelRunner,
656
+ topk: int,
657
+ speculative_num_steps: int,
658
+ ):
659
+ from sglang.srt.speculative.eagle_utils import generate_draft_decode_kv_indices
660
+
661
+ if topk > 1:
662
+ raise ValueError(
663
+ f"Currently Flashinfer MLA only supports topk=1 for speculative decoding"
664
+ )
665
+ self.topk = topk
666
+ self.speculative_num_steps = speculative_num_steps
667
+ self.generate_draft_decode_kv_indices = generate_draft_decode_kv_indices
668
+
669
+ max_bs = model_runner.req_to_token_pool.size * self.topk
670
+ self.kv_indptr = torch.zeros(
671
+ (
672
+ self.speculative_num_steps,
673
+ max_bs + 1,
674
+ ),
675
+ dtype=torch.int32,
676
+ device=model_runner.device,
677
+ )
678
+ self.q_indptr_decode = torch.arange(
679
+ 0, max_bs + 1, dtype=torch.int32, device=model_runner.device
680
+ )
681
+
682
+ self.attn_backends = []
683
+ for i in range(self.speculative_num_steps):
684
+ self.attn_backends.append(
685
+ FlashInferMLAAttnBackend(
686
+ model_runner,
687
+ skip_prefill=True,
688
+ kv_indptr_buf=self.kv_indptr[i],
689
+ q_indptr_decode_buf=self.q_indptr_decode,
690
+ )
691
+ )
692
+
693
+ self.max_context_len = self.attn_backends[0].max_context_len
694
+
695
+ # Cached variables for generate_draft_decode_kv_indices
696
+ self.pool_len = model_runner.req_to_token_pool.req_to_token.shape[1]
697
+
698
+ def common_template(
699
+ self,
700
+ forward_batch: ForwardBatch,
701
+ kv_indices_buffer: torch.Tensor,
702
+ call_fn: Callable,
703
+ ):
704
+ num_seqs = forward_batch.batch_size
705
+ bs = self.topk * num_seqs
706
+ seq_lens_sum = forward_batch.seq_lens_sum
707
+
708
+ self.generate_draft_decode_kv_indices[
709
+ (self.speculative_num_steps, num_seqs, self.topk)
710
+ ](
711
+ forward_batch.req_pool_indices,
712
+ forward_batch.req_to_token_pool.req_to_token,
713
+ forward_batch.seq_lens,
714
+ kv_indices_buffer,
715
+ self.kv_indptr,
716
+ forward_batch.positions,
717
+ num_seqs,
718
+ self.topk,
719
+ self.pool_len,
720
+ kv_indices_buffer.shape[1],
721
+ self.kv_indptr.shape[1],
722
+ triton.next_power_of_2(num_seqs),
723
+ triton.next_power_of_2(self.speculative_num_steps),
724
+ triton.next_power_of_2(bs),
725
+ )
726
+
727
+ assert forward_batch.spec_info is not None
728
+ assert isinstance(forward_batch.spec_info, EagleDraftInput)
729
+
730
+ for i in range(self.speculative_num_steps - 1):
731
+ forward_batch.spec_info.kv_indptr = self.kv_indptr[i, : bs + 1]
732
+ forward_batch.spec_info.kv_indices = kv_indices_buffer[i][
733
+ : seq_lens_sum * self.topk + bs * (i + 1)
734
+ ]
735
+ call_fn(i, forward_batch)
736
+
737
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
738
+ kv_indices = torch.zeros(
739
+ (
740
+ self.speculative_num_steps,
741
+ forward_batch.batch_size * self.topk * self.max_context_len,
742
+ ),
743
+ dtype=torch.int32,
744
+ device="cuda",
745
+ )
746
+
747
+ def call_fn(i, forward_batch):
748
+ assert forward_batch.spec_info is not None
749
+ assert isinstance(forward_batch.spec_info, EagleDraftInput)
750
+ forward_batch.spec_info.kv_indptr = (
751
+ forward_batch.spec_info.kv_indptr.clone()
752
+ )
753
+ forward_batch.spec_info.kv_indices = (
754
+ forward_batch.spec_info.kv_indices.clone()
755
+ )
756
+ self.attn_backends[i].init_forward_metadata(forward_batch)
757
+
758
+ self.common_template(forward_batch, kv_indices, call_fn)
759
+
760
+ def init_cuda_graph_state(self, max_bs: int):
761
+ self.cuda_graph_kv_indices = torch.zeros(
762
+ (self.speculative_num_steps, max_bs * self.max_context_len),
763
+ dtype=torch.int32,
764
+ device="cuda",
765
+ )
766
+
767
+ for i in range(self.speculative_num_steps):
768
+ self.attn_backends[i].init_cuda_graph_state(
769
+ max_bs, kv_indices_buf=self.cuda_graph_kv_indices[i]
770
+ )
771
+
772
+ def init_forward_metadata_capture_cuda_graph(self, forward_batch: ForwardBatch):
773
+ def call_fn(i, forward_batch):
774
+ self.attn_backends[i].init_forward_metadata_capture_cuda_graph(
775
+ forward_batch.batch_size,
776
+ forward_batch.batch_size * self.topk,
777
+ forward_batch.req_pool_indices,
778
+ forward_batch.seq_lens,
779
+ encoder_lens=None,
780
+ forward_mode=ForwardMode.DECODE,
781
+ spec_info=forward_batch.spec_info,
782
+ )
783
+
784
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
785
+
786
+ def init_forward_metadata_replay_cuda_graph(
787
+ self, forward_batch: ForwardBatch, bs: int
788
+ ):
789
+ def call_fn(i, forward_batch):
790
+ self.attn_backends[i].init_forward_metadata_replay_cuda_graph(
791
+ bs,
792
+ forward_batch.req_pool_indices,
793
+ forward_batch.seq_lens,
794
+ seq_lens_sum=-1,
795
+ encoder_lens=None,
796
+ forward_mode=ForwardMode.DECODE,
797
+ spec_info=forward_batch.spec_info,
798
+ seq_lens_cpu=forward_batch.seq_lens_cpu,
799
+ )
800
+
801
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
802
+
803
+
804
+ def fast_mla_decode_plan(
805
+ self,
806
+ qo_indptr_cpu: torch.Tensor,
807
+ kv_indptr_cpu: torch.Tensor,
808
+ kv_indices: torch.Tensor,
809
+ kv_len_arr_cpu: torch.Tensor,
810
+ num_heads: int,
811
+ head_dim_ckv: int,
812
+ head_dim_kpe: int,
813
+ page_size: int,
814
+ causal: bool,
815
+ sm_scale: float,
816
+ q_data_type: torch.dtype,
817
+ kv_data_type: torch.dtype,
818
+ ) -> None:
819
+ """A faster version of BatchMLAPagedAttentionWrapper::plan,
820
+ for skipping the stream synchronization in original plan function during
821
+ cuda graph replaying.
822
+ """
823
+ self._causal = causal
824
+ self._page_size = page_size
825
+ self._sm_scale = sm_scale
826
+
827
+ with self.device as device:
828
+ stream = torch.cuda.current_stream(device).cuda_stream
829
+ self._cached_module.plan(
830
+ self._float_workspace_buffer,
831
+ self._int_workspace_buffer,
832
+ self._pin_memory_int_workspace_buffer,
833
+ qo_indptr_cpu,
834
+ kv_indptr_cpu,
835
+ kv_len_arr_cpu,
836
+ num_heads,
837
+ head_dim_ckv,
838
+ causal,
839
+ stream,
840
+ )
lib/python3.12/site-packages/sglang/srt/layers/attention/flashmla_backend.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Support attention backend for FlashMLA.
5
+
6
+ #TODO
7
+ Enable speculative sampling in FlashMLA
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, Optional, Union
12
+
13
+ import torch
14
+ import triton
15
+ from flash_mla import flash_mla_with_kvcache, get_mla_metadata
16
+
17
+ from sglang.global_config import global_config
18
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
19
+ from sglang.srt.layers.attention.flashinfer_mla_backend import FlashInferMLAAttnBackend
20
+ from sglang.srt.layers.attention.utils import create_flashmla_kv_indices_triton
21
+ from sglang.srt.layers.dp_attention import get_attention_tp_size
22
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
23
+
24
+ if TYPE_CHECKING:
25
+ from sglang.srt.layers.radix_attention import RadixAttention
26
+ from sglang.srt.model_executor.model_runner import ModelRunner
27
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
28
+ from sglang.srt.speculative.spec_info import SpecInfo
29
+
30
+
31
+ # FlashMLA only supports pagesize=64
32
+ PAGE_SIZE = 64
33
+ # TODO The current setup is hard-coded and will be changed after integrating with MTP.
34
+ Q_LEN = 1
35
+
36
+
37
+ @dataclass
38
+ class FlashMLADecodeMetadata:
39
+ flashmla_metadata: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
40
+ num_splits: Optional[torch.Tensor] = None
41
+ block_kv_indices: Optional[torch.Tensor] = None
42
+
43
+ def __init__(
44
+ self,
45
+ flashmla_metadata: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
46
+ num_splits: Optional[torch.Tensor] = None,
47
+ block_kv_indices: Optional[torch.Tensor] = None,
48
+ ):
49
+ self.flashmla_metadata = flashmla_metadata
50
+ self.num_splits = num_splits
51
+ self.block_kv_indices = block_kv_indices
52
+
53
+
54
+ class FlashMLABackend(FlashInferMLAAttnBackend):
55
+ """Flashinfer attention kernels."""
56
+
57
+ def __init__(
58
+ self,
59
+ model_runner: ModelRunner,
60
+ skip_prefill: bool = False,
61
+ kv_indptr_buf: Optional[torch.Tensor] = None,
62
+ kv_last_page_len_buf: Optional[torch.Tensor] = None,
63
+ ):
64
+ super().__init__(
65
+ model_runner, skip_prefill, kv_indptr_buf, kv_last_page_len_buf
66
+ )
67
+
68
+ self.num_q_heads = (
69
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
70
+ )
71
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
72
+ self.num_local_heads = (
73
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
74
+ )
75
+ self.forward_metadata: Union[FlashMLADecodeMetadata] = None
76
+ self.kv_lora_rank = model_runner.model_config.kv_lora_rank
77
+ self.qk_nope_head_dim = model_runner.model_config.qk_nope_head_dim
78
+ self.qk_rope_head_dim = model_runner.model_config.qk_rope_head_dim
79
+ self.v_head_dim = model_runner.model_config.v_head_dim
80
+ self.scaling = model_runner.model_config.scaling
81
+ self.data_type = model_runner.kv_cache_dtype
82
+ self.q_data_type = model_runner.dtype
83
+ self.kv_cache_dim = self.kv_lora_rank + self.qk_rope_head_dim
84
+
85
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
86
+
87
+ bs = forward_batch.batch_size
88
+ spec_info = forward_batch.spec_info
89
+ if forward_batch.forward_mode.is_decode_or_idle():
90
+ if spec_info is None:
91
+ max_seqlen_pad = triton.cdiv(
92
+ forward_batch.seq_lens_cpu.max().item(), PAGE_SIZE
93
+ )
94
+ block_kv_indices = torch.full(
95
+ (bs, max_seqlen_pad),
96
+ -1,
97
+ dtype=torch.int32,
98
+ device=forward_batch.seq_lens.device,
99
+ )
100
+ create_flashmla_kv_indices_triton[(bs,)](
101
+ self.req_to_token,
102
+ forward_batch.req_pool_indices,
103
+ forward_batch.seq_lens,
104
+ None,
105
+ block_kv_indices,
106
+ self.req_to_token.stride(0),
107
+ max_seqlen_pad,
108
+ )
109
+ mla_metadata, num_splits = get_mla_metadata(
110
+ forward_batch.seq_lens.to(torch.int32),
111
+ Q_LEN * self.num_q_heads,
112
+ 1,
113
+ )
114
+ self.forward_metadata = FlashMLADecodeMetadata(
115
+ mla_metadata,
116
+ num_splits,
117
+ block_kv_indices,
118
+ )
119
+ else:
120
+ super().init_forward_metadata(forward_batch)
121
+ else:
122
+ super().init_forward_metadata(forward_batch)
123
+
124
+ def init_cuda_graph_state(
125
+ self,
126
+ max_bs: int,
127
+ block_kv_indices: Optional[torch.Tensor] = None,
128
+ ):
129
+ if block_kv_indices is None:
130
+ cuda_graph_kv_indices = torch.full(
131
+ (max_bs, (self.max_context_len + PAGE_SIZE) // PAGE_SIZE),
132
+ 1,
133
+ dtype=torch.int32,
134
+ device="cuda",
135
+ )
136
+ else:
137
+ cuda_graph_kv_indices = block_kv_indices
138
+
139
+ self.cuda_graph_mla_metadata, self.cuda_graph_num_splits = get_mla_metadata(
140
+ torch.ones(max_bs, dtype=torch.int32, device=cuda_graph_kv_indices.device),
141
+ Q_LEN * self.num_q_heads,
142
+ 1,
143
+ )
144
+ self.cuda_graph_kv_indices = cuda_graph_kv_indices
145
+
146
+ def init_forward_metadata_capture_cuda_graph(
147
+ self,
148
+ bs: int,
149
+ num_tokens: int,
150
+ req_pool_indices: torch.Tensor,
151
+ seq_lens: torch.Tensor,
152
+ encoder_lens: Optional[torch.Tensor],
153
+ forward_mode: ForwardMode,
154
+ spec_info: Optional[SpecInfo],
155
+ ):
156
+ if forward_mode.is_decode_or_idle():
157
+ if spec_info is None:
158
+ max_seqlen_pad = triton.cdiv(seq_lens.max().item(), PAGE_SIZE)
159
+
160
+ create_flashmla_kv_indices_triton[(bs,)](
161
+ self.req_to_token,
162
+ req_pool_indices,
163
+ seq_lens,
164
+ None,
165
+ self.cuda_graph_kv_indices,
166
+ self.req_to_token.stride(0),
167
+ self.cuda_graph_kv_indices.stride(0),
168
+ )
169
+ mla_metadata, num_splits = get_mla_metadata(
170
+ seq_lens.to(torch.int32),
171
+ Q_LEN * self.num_q_heads,
172
+ 1,
173
+ )
174
+ self.cuda_graph_mla_metadata.copy_(mla_metadata)
175
+ self.cuda_graph_num_splits[: bs + 1].copy_(num_splits)
176
+ self.forward_metadata = FlashMLADecodeMetadata(
177
+ self.cuda_graph_mla_metadata,
178
+ self.cuda_graph_num_splits[: bs + 1],
179
+ self.cuda_graph_kv_indices[:bs, :max_seqlen_pad],
180
+ )
181
+
182
+ else:
183
+ super().init_forward_metadata_capture_cuda_graph(
184
+ bs,
185
+ num_tokens,
186
+ req_pool_indices,
187
+ seq_lens,
188
+ encoder_lens,
189
+ forward_mode,
190
+ spec_info,
191
+ )
192
+
193
+ def init_forward_metadata_replay_cuda_graph(
194
+ self,
195
+ bs: int,
196
+ req_pool_indices: torch.Tensor,
197
+ seq_lens: torch.Tensor,
198
+ seq_lens_sum: int,
199
+ encoder_lens: Optional[torch.Tensor],
200
+ forward_mode: ForwardMode,
201
+ spec_info: Optional[SpecInfo],
202
+ seq_lens_cpu: Optional[torch.Tensor],
203
+ ):
204
+
205
+ if forward_mode.is_decode_or_idle():
206
+ assert seq_lens_cpu is not None
207
+ seq_lens = seq_lens[:bs]
208
+ seq_lens_cpu = seq_lens_cpu[:bs]
209
+ max_seqlen_pad = triton.cdiv(seq_lens_cpu.max().item(), PAGE_SIZE)
210
+ create_flashmla_kv_indices_triton[(bs,)](
211
+ self.req_to_token,
212
+ req_pool_indices[:bs],
213
+ seq_lens,
214
+ None,
215
+ self.cuda_graph_kv_indices,
216
+ self.req_to_token.stride(0),
217
+ self.cuda_graph_kv_indices.stride(0),
218
+ )
219
+ mla_metadata, num_splits = get_mla_metadata(
220
+ seq_lens.to(torch.int32),
221
+ Q_LEN * self.num_q_heads,
222
+ 1,
223
+ )
224
+ self.cuda_graph_mla_metadata.copy_(mla_metadata)
225
+ self.cuda_graph_num_splits[: bs + 1].copy_(num_splits)
226
+ self.forward_metadata.mla_metadata = self.cuda_graph_mla_metadata
227
+ self.forward_metadata.num_splits = self.cuda_graph_num_splits[: bs + 1]
228
+ self.forward_metadata.block_kv_indices = self.cuda_graph_kv_indices[
229
+ :bs, :max_seqlen_pad
230
+ ]
231
+
232
+ else:
233
+ super().init_forward_metadata_replay_cuda_graph(
234
+ bs,
235
+ req_pool_indices,
236
+ seq_lens,
237
+ seq_lens_sum,
238
+ encoder_lens,
239
+ forward_mode,
240
+ spec_info,
241
+ seq_lens_cpu,
242
+ )
243
+
244
+ def forward_decode(
245
+ self,
246
+ q: torch.Tensor,
247
+ k: torch.Tensor,
248
+ v: torch.Tensor,
249
+ layer: RadixAttention,
250
+ forward_batch: ForwardBatch,
251
+ save_kv_cache: bool = True,
252
+ ):
253
+ cache_loc = forward_batch.out_cache_loc
254
+
255
+ if k is not None:
256
+ assert v is not None
257
+ if save_kv_cache:
258
+ forward_batch.token_to_kv_pool.set_kv_buffer(
259
+ layer,
260
+ cache_loc,
261
+ k,
262
+ v,
263
+ )
264
+ bs = forward_batch.batch_size
265
+ k_cache = forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id)
266
+
267
+ reshape_q = q.view(bs, -1, layer.tp_q_head_num, layer.head_dim)
268
+
269
+ o, _ = flash_mla_with_kvcache(
270
+ q=reshape_q,
271
+ k_cache=k_cache.view(-1, PAGE_SIZE, 1, self.kv_cache_dim),
272
+ block_table=self.forward_metadata.block_kv_indices,
273
+ cache_seqlens=forward_batch.seq_lens.to(torch.int32),
274
+ head_dim_v=self.kv_lora_rank, # TODO Retrieve from config.
275
+ tile_scheduler_metadata=self.forward_metadata.flashmla_metadata,
276
+ num_splits=self.forward_metadata.num_splits,
277
+ softmax_scale=layer.scaling,
278
+ causal=False,
279
+ )
280
+
281
+ return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
lib/python3.12/site-packages/sglang/srt/layers/attention/torch_native_backend.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import torch
6
+ from torch.nn.functional import scaled_dot_product_attention
7
+
8
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
9
+ from sglang.srt.layers.radix_attention import AttentionType
10
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch
11
+
12
+ if TYPE_CHECKING:
13
+ from sglang.srt.layers.radix_attention import RadixAttention
14
+ from sglang.srt.model_executor.model_runner import ModelRunner
15
+
16
+
17
+ class TorchNativeAttnBackend(AttentionBackend):
18
+ def __init__(self, model_runner: ModelRunner):
19
+ super().__init__()
20
+ self.forward_metadata = None
21
+ self.device = model_runner.device
22
+
23
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
24
+ """Init the metadata for a forward pass."""
25
+ pass
26
+
27
+ def _run_sdpa_forward_extend(
28
+ self,
29
+ query: torch.Tensor,
30
+ output: torch.Tensor,
31
+ k_cache: torch.Tensor,
32
+ v_cache: torch.Tensor,
33
+ req_to_token: torch.Tensor,
34
+ req_pool_indices: torch.Tensor,
35
+ seq_lens: torch.Tensor,
36
+ extend_prefix_lens: torch.Tensor,
37
+ extend_seq_lens: torch.Tensor,
38
+ scaling=None,
39
+ enable_gqa=False,
40
+ causal=False,
41
+ ):
42
+ """Run the extend forward by using torch native sdpa op.
43
+
44
+ Args:
45
+ query: [num_tokens, num_heads, head_size]
46
+ output: [num_tokens, num_heads, head_size]
47
+ k_cache: [max_total_num_tokens, num_heads, head_size]
48
+ v_cache: [max_total_num_tokens, num_heads, head_size]
49
+ req_to_token: [max_num_reqs, max_context_len]
50
+ req_pool_indices: [num_seqs]
51
+ seq_lens: [num_seqs]
52
+ extend_prefix_lens: [num_seqs]
53
+ extend_seq_lens: [num_seqs]
54
+ scaling: float or None
55
+ enable_gqa: bool
56
+ causal: bool
57
+
58
+ Returns:
59
+ output: [num_tokens, num_heads, head_size]
60
+ """
61
+
62
+ assert seq_lens.shape[0] == extend_prefix_lens.shape[0]
63
+ assert seq_lens.shape[0] == extend_seq_lens.shape[0]
64
+
65
+ # [num_tokens, num_heads, head_size] -> [num_heads, num_tokens, head_size]
66
+ query = query.movedim(0, query.dim() - 2)
67
+
68
+ start_q, start_kv = 0, 0
69
+ for seq_idx in range(seq_lens.shape[0]):
70
+ # TODO: this loop process a sequence per iter, this is inefficient.
71
+ # Need optimize the performance later.
72
+
73
+ extend_seq_len_q = extend_seq_lens[seq_idx]
74
+ prefill_seq_len_q = extend_prefix_lens[seq_idx]
75
+
76
+ seq_len_kv = seq_lens[seq_idx]
77
+ end_q = start_q + extend_seq_len_q
78
+ end_kv = start_kv + seq_len_kv
79
+
80
+ per_req_query = query[:, start_q:end_q, :]
81
+ per_req_query_redudant = torch.empty(
82
+ (per_req_query.shape[0], seq_len_kv, per_req_query.shape[2]),
83
+ dtype=per_req_query.dtype,
84
+ device=per_req_query.device,
85
+ )
86
+
87
+ per_req_query_redudant[:, prefill_seq_len_q:, :] = per_req_query
88
+
89
+ # get key and value from cache. per_req_tokens contains the kv cache
90
+ # index for each token in the sequence.
91
+ req_pool_idx = req_pool_indices[seq_idx]
92
+ per_req_tokens = req_to_token[req_pool_idx, :seq_len_kv]
93
+ per_req_key = k_cache[per_req_tokens].movedim(0, query.dim() - 2)
94
+ per_req_value = v_cache[per_req_tokens].movedim(0, query.dim() - 2)
95
+
96
+ per_req_out_redudant = (
97
+ scaled_dot_product_attention(
98
+ per_req_query_redudant.unsqueeze(0),
99
+ per_req_key.unsqueeze(0),
100
+ per_req_value.unsqueeze(0),
101
+ enable_gqa=enable_gqa,
102
+ scale=scaling,
103
+ is_causal=causal,
104
+ )
105
+ .squeeze(0)
106
+ .movedim(query.dim() - 2, 0)
107
+ )
108
+ output[start_q:end_q, :, :] = per_req_out_redudant[prefill_seq_len_q:, :, :]
109
+ start_q, start_kv = end_q, end_kv
110
+ return output
111
+
112
+ def _run_sdpa_forward_decode(
113
+ self,
114
+ query: torch.Tensor,
115
+ output: torch.Tensor,
116
+ k_cache: torch.Tensor,
117
+ v_cache: torch.Tensor,
118
+ req_to_token: torch.Tensor,
119
+ req_pool_indices: torch.Tensor,
120
+ seq_lens: torch.Tensor,
121
+ scaling=None,
122
+ enable_gqa=False,
123
+ causal=False,
124
+ ):
125
+ """Run the decode forward by using torch native sdpa op.
126
+
127
+ Args:
128
+ query: [num_tokens, num_heads, head_size]
129
+ output: [num_tokens, num_heads, head_size]
130
+ k_cache: [max_total_num_tokens, num_heads, head_size]
131
+ v_cache: [max_total_num_tokens, num_heads, head_size]
132
+ req_to_token: [max_num_reqs, max_context_len]
133
+ req_pool_indices: [num_seqs]
134
+ seq_lens: [num_seqs]
135
+ scaling: float or None
136
+ enable_gqa: bool
137
+ causal: bool
138
+
139
+ Returns:
140
+ output: [num_tokens, num_heads, head_size]
141
+ """
142
+
143
+ # [num_tokens, num_heads, head_size] -> [num_heads, num_tokens, head_size]
144
+ query = query.movedim(0, query.dim() - 2)
145
+
146
+ start_q, start_kv = 0, 0
147
+ for seq_idx in range(seq_lens.shape[0]):
148
+ # TODO: this loop process a sequence per iter, this is inefficient.
149
+ # Need optimize the performance later.
150
+
151
+ seq_len_q = 1
152
+ seq_len_kv = seq_lens[seq_idx]
153
+ end_q = start_q + seq_len_q
154
+ end_kv = start_kv + seq_len_kv
155
+
156
+ per_req_query = query[:, start_q:end_q, :]
157
+
158
+ # get key and value from cache. per_req_tokens contains the kv cache
159
+ # index for each token in the sequence.
160
+ req_pool_idx = req_pool_indices[seq_idx]
161
+ per_req_tokens = req_to_token[req_pool_idx, :seq_len_kv]
162
+ per_req_key = k_cache[per_req_tokens].movedim(0, query.dim() - 2)
163
+ per_req_value = v_cache[per_req_tokens].movedim(0, query.dim() - 2)
164
+
165
+ per_req_out = (
166
+ scaled_dot_product_attention(
167
+ per_req_query.unsqueeze(0),
168
+ per_req_key.unsqueeze(0),
169
+ per_req_value.unsqueeze(0),
170
+ enable_gqa=enable_gqa,
171
+ scale=scaling,
172
+ is_causal=causal,
173
+ )
174
+ .squeeze(0)
175
+ .movedim(query.dim() - 2, 0)
176
+ )
177
+ output[start_q:end_q, :, :] = per_req_out
178
+ start_q, start_kv = end_q, end_kv
179
+
180
+ return output
181
+
182
+ def forward_extend(
183
+ self,
184
+ q,
185
+ k,
186
+ v,
187
+ layer: RadixAttention,
188
+ forward_batch: ForwardBatch,
189
+ save_kv_cache=True,
190
+ ):
191
+ if layer.qk_head_dim != layer.v_head_dim:
192
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
193
+ else:
194
+ o = torch.empty_like(q)
195
+
196
+ if save_kv_cache:
197
+ forward_batch.token_to_kv_pool.set_kv_buffer(
198
+ layer, forward_batch.out_cache_loc, k, v
199
+ )
200
+
201
+ use_gqa = layer.tp_q_head_num != layer.tp_k_head_num
202
+
203
+ q_ = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim)
204
+ o_ = o.view(-1, layer.tp_q_head_num, layer.v_head_dim)
205
+
206
+ causal = True
207
+ if layer.is_cross_attention or layer.attn_type == AttentionType.ENCODER_ONLY:
208
+ causal = False
209
+
210
+ self._run_sdpa_forward_extend(
211
+ q_,
212
+ o_,
213
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
214
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
215
+ forward_batch.req_to_token_pool.req_to_token,
216
+ forward_batch.req_pool_indices,
217
+ forward_batch.seq_lens,
218
+ forward_batch.extend_prefix_lens,
219
+ forward_batch.extend_seq_lens,
220
+ scaling=layer.scaling,
221
+ enable_gqa=use_gqa,
222
+ causal=causal,
223
+ )
224
+ return o
225
+
226
+ def forward_decode(
227
+ self,
228
+ q,
229
+ k,
230
+ v,
231
+ layer: RadixAttention,
232
+ forward_batch: ForwardBatch,
233
+ save_kv_cache=True,
234
+ ):
235
+ # During torch.compile, there is a bug in rotary_emb that causes the
236
+ # output value to have a 3D tensor shape. This reshapes the output correctly.
237
+ q = q.reshape(-1, layer.tp_q_head_num * layer.qk_head_dim)
238
+
239
+ if layer.qk_head_dim != layer.v_head_dim:
240
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
241
+ else:
242
+ o = torch.empty_like(q)
243
+
244
+ if save_kv_cache:
245
+ forward_batch.token_to_kv_pool.set_kv_buffer(
246
+ layer, forward_batch.out_cache_loc, k, v
247
+ )
248
+
249
+ use_gqa = layer.tp_q_head_num != layer.tp_k_head_num
250
+
251
+ q_ = q.view(-1, layer.tp_q_head_num, layer.qk_head_dim)
252
+ o_ = o.view(-1, layer.tp_q_head_num, layer.v_head_dim)
253
+
254
+ self._run_sdpa_forward_decode(
255
+ q_,
256
+ o_,
257
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
258
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
259
+ forward_batch.req_to_token_pool.req_to_token,
260
+ forward_batch.req_pool_indices,
261
+ forward_batch.seq_lens,
262
+ scaling=layer.scaling,
263
+ enable_gqa=use_gqa,
264
+ causal=False,
265
+ )
266
+
267
+ return o
lib/python3.12/site-packages/sglang/srt/layers/attention/triton_backend.py ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Optional, Union
5
+
6
+ import torch
7
+ import triton
8
+ import triton.language as tl
9
+
10
+ from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
11
+ from sglang.srt.layers.attention.utils import create_flashinfer_kv_indices_triton
12
+ from sglang.srt.layers.dp_attention import get_attention_tp_size
13
+ from sglang.srt.layers.radix_attention import AttentionType
14
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
15
+ from sglang.srt.utils import get_bool_env_var, get_device_core_count
16
+
17
+ if TYPE_CHECKING:
18
+ from sglang.srt.layers.radix_attention import RadixAttention
19
+ from sglang.srt.model_executor.model_runner import ModelRunner
20
+ from sglang.srt.speculative.eagle_utils import EagleDraftInput, EagleVerifyInput
21
+
22
+
23
+ @triton.jit
24
+ def get_num_kv_splits_triton(
25
+ num_kv_splits_ptr,
26
+ seq_lens_ptr,
27
+ num_seq,
28
+ num_group,
29
+ num_head,
30
+ num_kv_head,
31
+ max_kv_splits,
32
+ device_core_count,
33
+ MAX_NUM_SEQ: tl.constexpr,
34
+ ):
35
+ # TODO: this method is tunable, we need more online serving data to tune it
36
+ offs_seq = tl.arange(0, MAX_NUM_SEQ)
37
+ mask_seq = offs_seq < num_seq
38
+
39
+ seq_lens = tl.load(seq_lens_ptr + offs_seq, mask=mask_seq, other=0)
40
+ max_seq_len = tl.max(seq_lens)
41
+ seq_lens = tl.load(seq_lens_ptr + offs_seq, mask=mask_seq, other=max_seq_len)
42
+ min_seq_len = tl.min(seq_lens)
43
+ if max_seq_len * 8 < min_seq_len * 10:
44
+ min_seq_len = max_seq_len
45
+ max_kv_splits_1 = tl.minimum(tl.cdiv(max_seq_len, min_seq_len), max_kv_splits)
46
+ kv_chunk_size_1 = tl.cdiv(max_seq_len, max_kv_splits_1)
47
+
48
+ # NOTE: this is a hack to let num_kv_split grows up with seqlen gradually
49
+ ext_seq_len = tl.cast(max_seq_len, tl.float32) / 64.0
50
+ ext_device_core_count = tl.cast(
51
+ device_core_count * tl.maximum(tl.log2(ext_seq_len), 1.0), tl.int32
52
+ )
53
+ block_h, num_kv_group = 16, num_head // num_kv_head
54
+ if num_kv_group == 1:
55
+ token_grid = num_seq * num_group * num_head
56
+ else:
57
+ # from triton_ops/decode_attention.py:_decode_grouped_att_m_fwd
58
+ block_h = tl.minimum(block_h, num_kv_group)
59
+ token_grid = num_seq * num_group * tl.cdiv(num_head, block_h)
60
+ max_kv_splits_2 = tl.minimum(
61
+ tl.cdiv(ext_device_core_count, token_grid), max_kv_splits
62
+ )
63
+ kv_chunk_size_2 = tl.cdiv(max_seq_len, max_kv_splits_2)
64
+
65
+ num_kv_splits = tl.maximum(
66
+ tl.cdiv(seq_lens, kv_chunk_size_1), tl.cdiv(seq_lens, kv_chunk_size_2)
67
+ )
68
+
69
+ offs_token = offs_seq * num_group
70
+ mask_token = offs_token < num_seq * num_group
71
+ for i in range(0, num_group):
72
+ tl.store(num_kv_splits_ptr + i + offs_token, num_kv_splits, mask=mask_token)
73
+
74
+
75
+ @dataclass
76
+ class ForwardMetadata:
77
+ attn_logits: torch.Tensor
78
+ attn_lse: torch.Tensor
79
+ max_extend_len: int
80
+ num_kv_splits: torch.Tensor
81
+ kv_indptr: torch.Tensor
82
+ kv_indices: torch.Tensor
83
+ qo_indptr: torch.Tensor
84
+ custom_mask: torch.Tensor
85
+ mask_indptr: torch.Tensor
86
+
87
+
88
+ class TritonAttnBackend(AttentionBackend):
89
+ def __init__(
90
+ self,
91
+ model_runner: ModelRunner,
92
+ skip_prefill: bool = False,
93
+ kv_indptr_buf: Optional[torch.Tensor] = None,
94
+ ):
95
+ # Lazy import to avoid the initialization of cuda context
96
+ from sglang.srt.layers.attention.triton_ops.decode_attention import (
97
+ decode_attention_fwd,
98
+ )
99
+ from sglang.srt.layers.attention.triton_ops.extend_attention import (
100
+ extend_attention_fwd,
101
+ )
102
+
103
+ super().__init__()
104
+
105
+ self.decode_attention_fwd = decode_attention_fwd
106
+ self.extend_attention_fwd = extend_attention_fwd
107
+
108
+ self.skip_prefill = skip_prefill
109
+
110
+ max_bs = model_runner.req_to_token_pool.size
111
+
112
+ if kv_indptr_buf is None:
113
+ self.kv_indptr = torch.zeros(
114
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
115
+ )
116
+ else:
117
+ self.kv_indptr = kv_indptr_buf
118
+
119
+ self.req_to_token = model_runner.req_to_token_pool.req_to_token
120
+
121
+ if not self.skip_prefill:
122
+ self.qo_indptr = torch.zeros(
123
+ (max_bs + 1,), dtype=torch.int32, device=model_runner.device
124
+ )
125
+
126
+ self.mask_indptr = torch.zeros(
127
+ (max_bs + 1,), dtype=torch.int64, device=model_runner.device
128
+ )
129
+
130
+ self.num_draft_tokens = model_runner.server_args.speculative_num_draft_tokens
131
+
132
+ self.num_head = (
133
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
134
+ )
135
+ self.num_kv_head = model_runner.model_config.get_num_kv_heads(
136
+ get_attention_tp_size()
137
+ )
138
+
139
+ self.static_kv_splits = get_bool_env_var(
140
+ "SGLANG_TRITON_DECODE_ATTN_STATIC_KV_SPLITS", "false"
141
+ )
142
+ self.max_kv_splits = model_runner.server_args.triton_attention_num_kv_splits
143
+ self.v_head_dim = model_runner.token_to_kv_pool.get_value_buffer(0).shape[-1]
144
+
145
+ self.forward_metadata: ForwardMetadata = None
146
+
147
+ self.max_context_len = model_runner.model_config.context_len
148
+
149
+ self.device = model_runner.device
150
+ self.device_core_count = get_device_core_count(model_runner.gpu_id)
151
+
152
+ def get_num_kv_splits(
153
+ self,
154
+ num_kv_splits: torch.Tensor,
155
+ seq_lens: torch.Tensor,
156
+ ):
157
+ num_token, num_seq = num_kv_splits.shape[0], seq_lens.shape[0]
158
+ num_group = num_token // num_seq
159
+
160
+ assert (
161
+ num_group * num_seq == num_token
162
+ ), f"num_seq({num_seq}), num_token({num_token}), something goes wrong!"
163
+
164
+ if self.static_kv_splits or self.device_core_count <= 0:
165
+ num_kv_splits.fill_(self.max_kv_splits)
166
+ return
167
+
168
+ if num_seq < 256:
169
+ SCHEDULE_SEQ = 256
170
+ else:
171
+ SCHEDULE_SEQ = triton.next_power_of_2(num_seq)
172
+
173
+ get_num_kv_splits_triton[(1,)](
174
+ num_kv_splits,
175
+ seq_lens,
176
+ num_seq,
177
+ num_group,
178
+ self.num_head,
179
+ self.num_kv_head,
180
+ self.max_kv_splits,
181
+ self.device_core_count,
182
+ MAX_NUM_SEQ=SCHEDULE_SEQ,
183
+ )
184
+
185
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
186
+ """Init auxiliary variables for triton attention backend."""
187
+
188
+ bs = forward_batch.batch_size
189
+ kv_indptr = self.kv_indptr
190
+ spec_info = forward_batch.spec_info
191
+
192
+ if forward_batch.forward_mode.is_decode_or_idle():
193
+ if spec_info is None:
194
+ kv_indptr[1 : bs + 1] = torch.cumsum(forward_batch.seq_lens, dim=0)
195
+ kv_indptr = kv_indptr[: bs + 1]
196
+ kv_indices = torch.empty(
197
+ forward_batch.seq_lens_sum, dtype=torch.int32, device=self.device
198
+ )
199
+ create_flashinfer_kv_indices_triton[(bs,)](
200
+ self.req_to_token,
201
+ forward_batch.req_pool_indices,
202
+ forward_batch.seq_lens,
203
+ kv_indptr,
204
+ None,
205
+ kv_indices,
206
+ self.req_to_token.stride(0),
207
+ )
208
+ else:
209
+ kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
210
+ bs = kv_indptr.shape[0] - 1
211
+
212
+ attn_logits = torch.empty(
213
+ (bs, self.num_head, self.max_kv_splits, self.v_head_dim),
214
+ dtype=torch.float32,
215
+ device=self.device,
216
+ )
217
+ attn_lse = torch.empty(
218
+ (bs, self.num_head, self.max_kv_splits),
219
+ dtype=torch.float32,
220
+ device=self.device,
221
+ )
222
+ num_kv_splits = torch.empty((bs,), dtype=torch.int32, device=self.device)
223
+
224
+ self.get_num_kv_splits(num_kv_splits, forward_batch.seq_lens)
225
+
226
+ qo_indptr = None
227
+ custom_mask = None
228
+ mask_indptr = None
229
+ max_extend_len = None
230
+ elif forward_batch.forward_mode.is_target_verify():
231
+ bs = len(forward_batch.req_pool_indices)
232
+ qo_indptr = torch.arange(
233
+ 0,
234
+ (1 + bs) * self.num_draft_tokens,
235
+ step=self.num_draft_tokens,
236
+ dtype=torch.int32,
237
+ device=self.device,
238
+ )
239
+ # Different with flashinfer kv_indptr and kv_indices construction
240
+ kv_indptr[1 : bs + 1] = torch.cumsum(forward_batch.seq_lens, dim=0)
241
+ kv_indptr = kv_indptr[: bs + 1]
242
+ kv_indices = torch.empty(
243
+ kv_indptr[-1], dtype=torch.int32, device=self.device
244
+ )
245
+ create_flashinfer_kv_indices_triton[(bs,)](
246
+ self.req_to_token,
247
+ forward_batch.req_pool_indices,
248
+ forward_batch.seq_lens,
249
+ kv_indptr,
250
+ None,
251
+ kv_indices,
252
+ self.req_to_token.stride(0),
253
+ )
254
+
255
+ custom_mask = spec_info.custom_mask
256
+ seq_mask_len = self.num_draft_tokens * (
257
+ forward_batch.seq_lens + self.num_draft_tokens
258
+ )
259
+ mask_indptr = self.mask_indptr
260
+ mask_indptr[1 : bs + 1] = torch.cumsum(seq_mask_len[:bs], dim=0)
261
+ mask_indptr = mask_indptr[: bs + 1]
262
+ max_extend_len = self.num_draft_tokens
263
+ num_kv_splits = None
264
+ attn_logits = None
265
+ attn_lse = None
266
+ elif forward_batch.forward_mode.is_draft_extend():
267
+ kv_indices, kv_indptr, qo_indptr, custom_mask = (
268
+ spec_info.generate_attn_arg_prefill(
269
+ forward_batch.req_pool_indices,
270
+ forward_batch.seq_lens,
271
+ None,
272
+ self.req_to_token,
273
+ )
274
+ )
275
+ mask_indptr = None
276
+ # TODO(FIXME): This will trigger an invalid Eagle tree when using
277
+ # `max(spec_info.accept_length_cpu)`.
278
+ # It might have been forgotten to update somewhere.
279
+ max_extend_len = torch.max(spec_info.accept_length).item()
280
+ num_kv_splits = None
281
+ attn_logits = None
282
+ attn_lse = None
283
+ else:
284
+ kv_indptr[1 : bs + 1] = torch.cumsum(
285
+ forward_batch.extend_prefix_lens, dim=0
286
+ )
287
+ kv_indptr = kv_indptr[: bs + 1]
288
+ kv_indices = torch.empty(
289
+ forward_batch.extend_prefix_lens.sum().item(),
290
+ dtype=torch.int32,
291
+ device=self.device,
292
+ )
293
+ create_flashinfer_kv_indices_triton[(bs,)](
294
+ self.req_to_token,
295
+ forward_batch.req_pool_indices,
296
+ forward_batch.extend_prefix_lens,
297
+ kv_indptr,
298
+ None,
299
+ kv_indices,
300
+ self.req_to_token.stride(0),
301
+ )
302
+
303
+ qo_indptr = self.qo_indptr
304
+ qo_indptr[1 : bs + 1] = torch.cumsum(forward_batch.extend_seq_lens, dim=0)
305
+ qo_indptr = qo_indptr[: bs + 1]
306
+ custom_mask = None
307
+ mask_indptr = None
308
+ attn_logits = None
309
+ attn_lse = None
310
+ max_extend_len = torch.max(forward_batch.extend_seq_lens).item()
311
+ num_kv_splits = None
312
+
313
+ self.forward_metadata = ForwardMetadata(
314
+ attn_logits,
315
+ attn_lse,
316
+ max_extend_len,
317
+ num_kv_splits,
318
+ kv_indptr,
319
+ kv_indices,
320
+ qo_indptr,
321
+ custom_mask,
322
+ mask_indptr,
323
+ )
324
+
325
+ def init_cuda_graph_state(
326
+ self, max_bs: int, kv_indices_buf: Optional[torch.Tensor] = None
327
+ ):
328
+ self.cuda_graph_attn_logits = torch.zeros(
329
+ (max_bs, self.num_head, self.max_kv_splits, self.v_head_dim),
330
+ dtype=torch.float32,
331
+ device=self.device,
332
+ )
333
+ self.cuda_graph_attn_lse = torch.zeros(
334
+ (max_bs, self.num_head, self.max_kv_splits),
335
+ dtype=torch.float32,
336
+ device=self.device,
337
+ )
338
+ self.cuda_graph_num_kv_splits = torch.full(
339
+ (max_bs,), self.max_kv_splits, dtype=torch.int32, device=self.device
340
+ )
341
+ if kv_indices_buf is None:
342
+ self.cuda_graph_kv_indices = torch.zeros(
343
+ (max_bs * self.max_context_len),
344
+ dtype=torch.int32,
345
+ device=self.device,
346
+ )
347
+ else:
348
+ self.cuda_graph_kv_indices = kv_indices_buf
349
+
350
+ if not self.skip_prefill:
351
+ self.cuda_graph_custom_mask = torch.zeros(
352
+ (max_bs * self.max_context_len),
353
+ dtype=torch.uint8,
354
+ device=self.device,
355
+ )
356
+
357
+ def init_forward_metadata_capture_cuda_graph(
358
+ self,
359
+ bs: int,
360
+ num_tokens: int,
361
+ req_pool_indices: torch.Tensor,
362
+ seq_lens: torch.Tensor,
363
+ encoder_lens: Optional[torch.Tensor],
364
+ forward_mode: ForwardMode,
365
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
366
+ ):
367
+ assert encoder_lens is None, "Not supported"
368
+
369
+ if forward_mode.is_decode_or_idle():
370
+ if spec_info is None:
371
+ kv_indptr = self.kv_indptr
372
+ kv_indptr[1 : bs + 1] = torch.cumsum(seq_lens, dim=0)
373
+ kv_indptr = kv_indptr[: bs + 1]
374
+ kv_indices = self.cuda_graph_kv_indices
375
+ create_flashinfer_kv_indices_triton[(bs,)](
376
+ self.req_to_token,
377
+ req_pool_indices,
378
+ seq_lens,
379
+ kv_indptr,
380
+ None,
381
+ kv_indices,
382
+ self.req_to_token.stride(0),
383
+ )
384
+ else:
385
+ kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
386
+
387
+ attn_logits = self.cuda_graph_attn_logits
388
+ attn_lse = self.cuda_graph_attn_lse
389
+ max_extend_len = None
390
+ num_kv_splits = self.cuda_graph_num_kv_splits
391
+ qo_indptr = None
392
+ custom_mask = None
393
+ mask_indptr = None
394
+ elif forward_mode.is_target_verify():
395
+ qo_indptr = self.qo_indptr[: bs + 1]
396
+ qo_indptr[: bs + 1] = torch.arange(
397
+ 0,
398
+ (1 + bs) * self.num_draft_tokens,
399
+ step=self.num_draft_tokens,
400
+ dtype=torch.int32,
401
+ device=self.device,
402
+ )
403
+ kv_indptr = self.kv_indptr[: bs + 1]
404
+ kv_indptr[1 : bs + 1] = torch.cumsum(seq_lens, dim=0)
405
+ kv_indices = self.cuda_graph_kv_indices
406
+ create_flashinfer_kv_indices_triton[(bs,)](
407
+ self.req_to_token,
408
+ req_pool_indices,
409
+ seq_lens,
410
+ kv_indptr,
411
+ None,
412
+ kv_indices,
413
+ self.req_to_token.stride(0),
414
+ )
415
+
416
+ custom_mask = self.cuda_graph_custom_mask
417
+ seq_mask_len = self.num_draft_tokens * (seq_lens + self.num_draft_tokens)
418
+ mask_indptr = self.mask_indptr[: bs + 1]
419
+ mask_indptr[1 : bs + 1] = torch.cumsum(seq_mask_len, dim=0)
420
+ max_extend_len = self.num_draft_tokens
421
+ num_kv_splits = None
422
+ attn_logits = None
423
+ attn_lse = None
424
+ else:
425
+ raise ValueError(
426
+ f"Invalid forward mode: {forward_mode=} for CUDA Graph capture."
427
+ )
428
+
429
+ self.forward_metadata = ForwardMetadata(
430
+ attn_logits,
431
+ attn_lse,
432
+ max_extend_len,
433
+ num_kv_splits,
434
+ kv_indptr,
435
+ kv_indices,
436
+ qo_indptr,
437
+ custom_mask,
438
+ mask_indptr,
439
+ )
440
+
441
+ def init_forward_metadata_replay_cuda_graph(
442
+ self,
443
+ bs: int,
444
+ req_pool_indices: torch.Tensor,
445
+ seq_lens: torch.Tensor,
446
+ seq_lens_sum: int,
447
+ encoder_lens: Optional[torch.Tensor],
448
+ forward_mode: ForwardMode,
449
+ spec_info: Optional[Union[EagleDraftInput, EagleVerifyInput]],
450
+ seq_lens_cpu: Optional[torch.Tensor],
451
+ ):
452
+ # NOTE: encoder_lens expected to be zeros or None
453
+ if forward_mode.is_decode_or_idle():
454
+ # Update kv_indptr, kv_indices
455
+ kv_indptr = self.kv_indptr
456
+ kv_indices = self.cuda_graph_kv_indices
457
+ num_kv_splits = self.cuda_graph_num_kv_splits
458
+ if spec_info is None:
459
+ kv_indptr[1 : bs + 1] = torch.cumsum(seq_lens[:bs], dim=0)
460
+ kv_indptr = kv_indptr[: bs + 1]
461
+ create_flashinfer_kv_indices_triton[(bs,)](
462
+ self.req_to_token,
463
+ req_pool_indices[:bs],
464
+ seq_lens[:bs],
465
+ kv_indptr,
466
+ None,
467
+ kv_indices,
468
+ self.req_to_token.stride(0),
469
+ )
470
+ num_token = bs
471
+ else:
472
+ kv_indptr[: spec_info.kv_indptr.shape[0]] = spec_info.kv_indptr
473
+ kv_indices[: spec_info.kv_indices.shape[0]] = spec_info.kv_indices
474
+ num_token = spec_info.kv_indptr.shape[0] - 1
475
+ self.get_num_kv_splits(num_kv_splits[:num_token], seq_lens[:bs])
476
+ elif forward_mode.is_target_verify():
477
+ # Update qo_indptr, kv_indptr, kv_indices, custom_mask, mask_indptr
478
+ bs = len(req_pool_indices)
479
+ qo_indptr = self.qo_indptr[: bs + 1]
480
+ qo_indptr[: bs + 1] = torch.arange(
481
+ 0,
482
+ (1 + bs) * self.num_draft_tokens,
483
+ step=self.num_draft_tokens,
484
+ dtype=torch.int32,
485
+ device=self.device,
486
+ )
487
+ kv_indptr = self.kv_indptr[: bs + 1]
488
+ kv_indptr[1 : bs + 1] = torch.cumsum(seq_lens, dim=0)
489
+ kv_indices = self.cuda_graph_kv_indices
490
+ create_flashinfer_kv_indices_triton[(bs,)](
491
+ self.req_to_token,
492
+ req_pool_indices,
493
+ seq_lens,
494
+ kv_indptr,
495
+ None,
496
+ kv_indices,
497
+ self.req_to_token.stride(0),
498
+ )
499
+ custom_mask = self.cuda_graph_custom_mask
500
+ custom_mask[: spec_info.custom_mask.shape[0]] = spec_info.custom_mask
501
+ seq_mask_len = self.num_draft_tokens * (seq_lens + self.num_draft_tokens)
502
+ mask_indptr = self.mask_indptr[: bs + 1]
503
+ mask_indptr[1 : bs + 1] = torch.cumsum(seq_mask_len, dim=0)
504
+ else:
505
+ raise ValueError(
506
+ f"Invalid forward mode: {forward_mode=} for CUDA Graph replay."
507
+ )
508
+
509
+ def get_cuda_graph_seq_len_fill_value(self):
510
+ return 1
511
+
512
+ def forward_extend(
513
+ self,
514
+ q: torch.Tensor,
515
+ k: torch.Tensor,
516
+ v: torch.Tensor,
517
+ layer: RadixAttention,
518
+ forward_batch: ForwardBatch,
519
+ save_kv_cache=True,
520
+ ):
521
+ # TODO: reuse the buffer across layers
522
+ if layer.qk_head_dim != layer.v_head_dim:
523
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
524
+ else:
525
+ o = torch.empty_like(q)
526
+
527
+ if save_kv_cache:
528
+ forward_batch.token_to_kv_pool.set_kv_buffer(
529
+ layer, forward_batch.out_cache_loc, k, v
530
+ )
531
+
532
+ causal = True
533
+ if layer.attn_type == AttentionType.ENCODER_ONLY:
534
+ causal = False
535
+
536
+ self.extend_attention_fwd(
537
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
538
+ k.contiguous(),
539
+ v.contiguous(),
540
+ o.view(-1, layer.tp_q_head_num, layer.v_head_dim),
541
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
542
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
543
+ self.forward_metadata.qo_indptr,
544
+ self.forward_metadata.kv_indptr,
545
+ self.forward_metadata.kv_indices,
546
+ self.forward_metadata.custom_mask,
547
+ causal,
548
+ self.forward_metadata.mask_indptr,
549
+ self.forward_metadata.max_extend_len,
550
+ layer.scaling,
551
+ layer.logit_cap,
552
+ )
553
+ return o
554
+
555
+ def forward_decode(
556
+ self,
557
+ q: torch.Tensor,
558
+ k: torch.Tensor,
559
+ v: torch.Tensor,
560
+ layer: RadixAttention,
561
+ forward_batch: ForwardBatch,
562
+ save_kv_cache=True,
563
+ ):
564
+ # During torch.compile, there is a bug in rotary_emb that causes the
565
+ # output value to have a 3D tensor shape. This reshapes the output correctly.
566
+ q = q.reshape(-1, layer.tp_q_head_num * layer.qk_head_dim)
567
+
568
+ # TODO: reuse the buffer across layers
569
+ if layer.qk_head_dim != layer.v_head_dim:
570
+ o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
571
+ else:
572
+ o = torch.empty_like(q)
573
+
574
+ if save_kv_cache:
575
+ forward_batch.token_to_kv_pool.set_kv_buffer(
576
+ layer, forward_batch.out_cache_loc, k, v
577
+ )
578
+
579
+ self.decode_attention_fwd(
580
+ q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
581
+ forward_batch.token_to_kv_pool.get_key_buffer(layer.layer_id),
582
+ forward_batch.token_to_kv_pool.get_value_buffer(layer.layer_id),
583
+ o.view(-1, layer.tp_q_head_num, layer.v_head_dim),
584
+ self.forward_metadata.kv_indptr,
585
+ self.forward_metadata.kv_indices,
586
+ self.forward_metadata.attn_logits,
587
+ self.forward_metadata.attn_lse,
588
+ self.forward_metadata.num_kv_splits,
589
+ self.max_kv_splits,
590
+ layer.scaling,
591
+ layer.logit_cap,
592
+ )
593
+ return o
594
+
595
+
596
+ class TritonMultiStepDraftBackend:
597
+ """
598
+ Wrap multiple triton attention backends as one for multiple consecutive
599
+ draft decoding steps.
600
+ """
601
+
602
+ def __init__(
603
+ self,
604
+ model_runner: ModelRunner,
605
+ topk: int,
606
+ speculative_num_steps: int,
607
+ ):
608
+ from sglang.srt.speculative.eagle_utils import generate_draft_decode_kv_indices
609
+
610
+ self.topk = topk
611
+ self.speculative_num_steps = speculative_num_steps
612
+ self.generate_draft_decode_kv_indices = generate_draft_decode_kv_indices
613
+ max_bs = model_runner.req_to_token_pool.size * self.topk
614
+ self.kv_indptr = torch.zeros(
615
+ (
616
+ self.speculative_num_steps,
617
+ max_bs + 1,
618
+ ),
619
+ dtype=torch.int32,
620
+ device=model_runner.device,
621
+ )
622
+ self.attn_backends = []
623
+ for i in range(self.speculative_num_steps):
624
+ self.attn_backends.append(
625
+ TritonAttnBackend(
626
+ model_runner,
627
+ skip_prefill=True,
628
+ kv_indptr_buf=self.kv_indptr[i],
629
+ )
630
+ )
631
+ self.max_context_len = self.attn_backends[0].max_context_len
632
+ self.num_head = (
633
+ model_runner.model_config.num_attention_heads // get_attention_tp_size()
634
+ )
635
+ self.device = model_runner.device
636
+ # Cached variables for generate_draft_decode_kv_indices
637
+ self.pool_len = model_runner.req_to_token_pool.req_to_token.shape[1]
638
+
639
+ def common_template(
640
+ self, forward_batch: ForwardBatch, kv_indices_buffer: torch.Tensor, call_fn: int
641
+ ):
642
+ num_seqs = forward_batch.batch_size
643
+ bs = self.topk * num_seqs
644
+ seq_lens_sum = forward_batch.seq_lens_sum
645
+
646
+ self.generate_draft_decode_kv_indices[
647
+ (self.speculative_num_steps, num_seqs, self.topk)
648
+ ](
649
+ forward_batch.req_pool_indices,
650
+ forward_batch.req_to_token_pool.req_to_token,
651
+ forward_batch.seq_lens,
652
+ kv_indices_buffer,
653
+ self.kv_indptr,
654
+ forward_batch.positions,
655
+ num_seqs,
656
+ self.topk,
657
+ self.pool_len,
658
+ kv_indices_buffer.shape[1],
659
+ self.kv_indptr.shape[1],
660
+ triton.next_power_of_2(num_seqs),
661
+ triton.next_power_of_2(self.speculative_num_steps),
662
+ triton.next_power_of_2(bs),
663
+ )
664
+
665
+ for i in range(self.speculative_num_steps):
666
+ forward_batch.spec_info.kv_indptr = self.kv_indptr[i, : bs + 1]
667
+ forward_batch.spec_info.kv_indices = kv_indices_buffer[i][
668
+ : seq_lens_sum * self.topk + bs * (i + 1)
669
+ ]
670
+ call_fn(i, forward_batch)
671
+
672
+ def init_forward_metadata(self, forward_batch: ForwardBatch):
673
+ kv_indices = torch.empty(
674
+ (
675
+ self.speculative_num_steps,
676
+ forward_batch.batch_size * self.topk * self.max_context_len,
677
+ ),
678
+ dtype=torch.int32,
679
+ device=self.device,
680
+ )
681
+
682
+ def call_fn(i, forward_batch):
683
+ forward_batch.spec_info.kv_indptr = (
684
+ forward_batch.spec_info.kv_indptr.clone()
685
+ )
686
+ forward_batch.spec_info.kv_indices = (
687
+ forward_batch.spec_info.kv_indices.clone()
688
+ )
689
+ self.attn_backends[i].init_forward_metadata(forward_batch)
690
+
691
+ self.common_template(forward_batch, kv_indices, call_fn)
692
+
693
+ def init_cuda_graph_state(self, max_bs: int):
694
+ self.cuda_graph_kv_indices = torch.zeros(
695
+ (self.speculative_num_steps, max_bs * self.max_context_len),
696
+ dtype=torch.int32,
697
+ device=self.device,
698
+ )
699
+ for i in range(self.speculative_num_steps):
700
+ self.attn_backends[i].init_cuda_graph_state(
701
+ max_bs, kv_indices_buf=self.cuda_graph_kv_indices[i]
702
+ )
703
+
704
+ def init_forward_metadata_capture_cuda_graph(self, forward_batch: ForwardBatch):
705
+ def call_fn(i, forward_batch):
706
+ self.attn_backends[i].init_forward_metadata_capture_cuda_graph(
707
+ forward_batch.batch_size,
708
+ forward_batch.batch_size * self.topk,
709
+ forward_batch.req_pool_indices,
710
+ forward_batch.seq_lens,
711
+ encoder_lens=None,
712
+ forward_mode=ForwardMode.DECODE,
713
+ spec_info=forward_batch.spec_info,
714
+ )
715
+
716
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
717
+
718
+ def init_forward_metadata_replay_cuda_graph(
719
+ self, forward_batch: ForwardBatch, bs: int
720
+ ):
721
+ def call_fn(i, forward_batch):
722
+ self.attn_backends[i].init_forward_metadata_replay_cuda_graph(
723
+ bs,
724
+ forward_batch.req_pool_indices,
725
+ forward_batch.seq_lens,
726
+ seq_lens_sum=-1,
727
+ encoder_lens=None,
728
+ forward_mode=ForwardMode.DECODE,
729
+ spec_info=forward_batch.spec_info,
730
+ seq_lens_cpu=None,
731
+ )
732
+
733
+ self.common_template(forward_batch, self.cuda_graph_kv_indices, call_fn)
lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/decode_attention.py ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023-2024 SGLang Team
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ==============================================================================
14
+ """
15
+ Memory-efficient attention for decoding.
16
+ It supports page size = 1.
17
+ """
18
+
19
+ # Adapted from
20
+ # https://github.com/ModelTC/lightllm/blob/96353e868a840db4d103138caf15ed9dbea8c186/lightllm/models/deepseek2/triton_kernel/gqa_flash_decoding_stage1.py
21
+ # https://github.com/ModelTC/lightllm/blob/96353e868a840db4d103138caf15ed9dbea8c186/lightllm/models/deepseek2/triton_kernel/gqa_flash_decoding_stage2.py
22
+
23
+ import logging
24
+
25
+ import triton
26
+ import triton.language as tl
27
+
28
+ from sglang.srt.utils import is_hip
29
+
30
+ _is_hip = is_hip()
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # TODO: Remove this when triton>=3.2.0. This issue will not affect performance and accuracy.
35
+ logger.warning(
36
+ "The following error message 'operation scheduled before its operands' can be ignored."
37
+ )
38
+
39
+
40
+ _MIN_BLOCK_KV = 32
41
+
42
+
43
+ @triton.jit
44
+ def tanh(x):
45
+ # Tanh is just a scaled sigmoid
46
+ return 2 * tl.sigmoid(2 * x) - 1
47
+
48
+
49
+ @triton.jit
50
+ def _fwd_kernel_stage1(
51
+ Q,
52
+ K_Buffer,
53
+ V_Buffer,
54
+ sm_scale,
55
+ kv_indptr,
56
+ kv_indices,
57
+ Att_Out,
58
+ Att_Lse,
59
+ num_kv_splits,
60
+ stride_qbs,
61
+ stride_qh,
62
+ stride_buf_kbs,
63
+ stride_buf_kh,
64
+ stride_buf_vbs,
65
+ stride_buf_vh,
66
+ stride_mid_ob,
67
+ stride_mid_oh,
68
+ stride_mid_os,
69
+ kv_group_num: tl.constexpr,
70
+ BLOCK_DMODEL: tl.constexpr,
71
+ BLOCK_DV: tl.constexpr,
72
+ BLOCK_N: tl.constexpr,
73
+ MIN_BLOCK_KV: tl.constexpr,
74
+ logit_cap: tl.constexpr,
75
+ Lk: tl.constexpr,
76
+ Lv: tl.constexpr,
77
+ ):
78
+ cur_batch = tl.program_id(0)
79
+ cur_head = tl.program_id(1)
80
+ split_kv_id = tl.program_id(2)
81
+
82
+ cur_kv_head = cur_head // kv_group_num
83
+
84
+ offs_d = tl.arange(0, BLOCK_DMODEL)
85
+ offs_dv = tl.arange(0, BLOCK_DV)
86
+ mask_d = offs_d < Lk
87
+ mask_dv = offs_dv < Lv
88
+
89
+ cur_batch_kv_start_idx = tl.load(kv_indptr + cur_batch)
90
+ cur_batch_seq_len = tl.load(kv_indptr + cur_batch + 1) - cur_batch_kv_start_idx
91
+ kv_splits = tl.load(num_kv_splits + cur_batch)
92
+
93
+ off_q = cur_batch * stride_qbs + cur_head * stride_qh + offs_d
94
+
95
+ kv_len_per_split = (
96
+ tl.cdiv(tl.cdiv(cur_batch_seq_len, kv_splits), MIN_BLOCK_KV) * MIN_BLOCK_KV
97
+ )
98
+ split_kv_start = kv_len_per_split * split_kv_id
99
+ split_kv_end = tl.minimum(split_kv_start + kv_len_per_split, cur_batch_seq_len)
100
+
101
+ e_max = -float("inf")
102
+ e_sum = 0.0
103
+ acc = tl.zeros([BLOCK_DV], dtype=tl.float32)
104
+
105
+ if split_kv_end > split_kv_start:
106
+ q = tl.load(Q + off_q, mask=mask_d, other=0.0)
107
+ for start_n in range(split_kv_start, split_kv_end, BLOCK_N):
108
+ offs_n = start_n + tl.arange(0, BLOCK_N)
109
+ kv_loc = tl.load(
110
+ kv_indices + cur_batch_kv_start_idx + offs_n,
111
+ mask=offs_n < split_kv_end,
112
+ other=0,
113
+ )
114
+ offs_buf_k = (
115
+ kv_loc[:, None] * stride_buf_kbs
116
+ + cur_kv_head * stride_buf_kh
117
+ + offs_d[None, :]
118
+ )
119
+ k = tl.load(
120
+ K_Buffer + offs_buf_k,
121
+ mask=(offs_n[:, None] < split_kv_end) & (mask_d[None, :]),
122
+ other=0.0,
123
+ )
124
+ qk = tl.sum(q[None, :] * k, 1)
125
+ qk *= sm_scale
126
+
127
+ if logit_cap > 0:
128
+ qk = logit_cap * tanh(qk / logit_cap)
129
+
130
+ qk = tl.where(offs_n < split_kv_end, qk, float("-inf"))
131
+
132
+ offs_buf_v = (
133
+ kv_loc[:, None] * stride_buf_vbs
134
+ + cur_kv_head * stride_buf_vh
135
+ + offs_dv[None, :]
136
+ )
137
+ v = tl.load(
138
+ V_Buffer + offs_buf_v,
139
+ mask=(offs_n[:, None] < split_kv_end) & (mask_dv[None, :]),
140
+ other=0.0,
141
+ )
142
+
143
+ n_e_max = tl.maximum(tl.max(qk, 0), e_max)
144
+ re_scale = tl.exp(e_max - n_e_max)
145
+ p = tl.exp(qk - n_e_max)
146
+ acc *= re_scale
147
+ acc += tl.sum(p[:, None] * v, 0)
148
+
149
+ e_sum = e_sum * re_scale + tl.sum(p, 0)
150
+ e_max = n_e_max
151
+
152
+ offs_mid_o = (
153
+ cur_batch * stride_mid_ob
154
+ + cur_head * stride_mid_oh
155
+ + split_kv_id * stride_mid_os
156
+ + offs_dv
157
+ )
158
+
159
+ tl.store(
160
+ Att_Out + offs_mid_o,
161
+ acc / e_sum,
162
+ mask=(mask_dv),
163
+ )
164
+
165
+ offs_mid_o_1 = (
166
+ cur_batch * stride_mid_ob
167
+ + cur_head * stride_mid_oh
168
+ + split_kv_id * stride_mid_os
169
+ ) // Lv
170
+
171
+ tl.store(
172
+ Att_Lse + offs_mid_o_1,
173
+ e_max + tl.log(e_sum),
174
+ )
175
+
176
+
177
+ def _decode_att_m_fwd(
178
+ q,
179
+ k_buffer,
180
+ v_buffer,
181
+ att_out,
182
+ att_lse,
183
+ kv_indptr,
184
+ kv_indices,
185
+ num_kv_splits,
186
+ max_kv_splits,
187
+ sm_scale,
188
+ logit_cap,
189
+ ):
190
+ BLOCK = 64
191
+ # [TODO] work around SGPR limit on MI3xx
192
+ if _is_hip:
193
+ BLOCK = 8
194
+ MAX_KV_SPLITS = max_kv_splits
195
+ Lk = k_buffer.shape[-1]
196
+ Lv = v_buffer.shape[-1]
197
+
198
+ batch, head_num = kv_indptr.shape[0] - 1, q.shape[1]
199
+
200
+ grid = (batch, head_num, MAX_KV_SPLITS)
201
+ kv_group_num = q.shape[1] // k_buffer.shape[1]
202
+
203
+ if kv_group_num == 1:
204
+ num_warps = 4
205
+ else:
206
+ num_warps = 2
207
+ if _is_hip:
208
+ num_warps = 1
209
+
210
+ BLOCK_DMODEL = triton.next_power_of_2(Lk)
211
+ BLOCK_DV = triton.next_power_of_2(Lv)
212
+
213
+ _fwd_kernel_stage1[grid](
214
+ q,
215
+ k_buffer,
216
+ v_buffer,
217
+ sm_scale,
218
+ kv_indptr,
219
+ kv_indices,
220
+ att_out,
221
+ att_lse,
222
+ num_kv_splits,
223
+ q.stride(0),
224
+ q.stride(1),
225
+ k_buffer.stride(0),
226
+ k_buffer.stride(1),
227
+ v_buffer.stride(0),
228
+ v_buffer.stride(1),
229
+ att_out.stride(0),
230
+ att_out.stride(1),
231
+ att_out.stride(2),
232
+ kv_group_num=kv_group_num,
233
+ BLOCK_DMODEL=BLOCK_DMODEL,
234
+ BLOCK_DV=BLOCK_DV,
235
+ BLOCK_N=BLOCK,
236
+ MIN_BLOCK_KV=_MIN_BLOCK_KV,
237
+ logit_cap=logit_cap,
238
+ num_warps=num_warps,
239
+ num_stages=2,
240
+ Lk=Lk,
241
+ Lv=Lv,
242
+ )
243
+
244
+
245
+ @triton.jit
246
+ def _fwd_grouped_kernel_stage1(
247
+ Q,
248
+ K_Buffer,
249
+ V_Buffer,
250
+ sm_scale,
251
+ kv_indptr,
252
+ kv_indices,
253
+ Att_Out,
254
+ Att_Lse,
255
+ num_kv_splits,
256
+ stride_qbs,
257
+ stride_qh,
258
+ stride_buf_kbs,
259
+ stride_buf_kh,
260
+ stride_buf_vbs,
261
+ stride_buf_vh,
262
+ stride_mid_ob,
263
+ stride_mid_oh,
264
+ stride_mid_os,
265
+ kv_group_num: tl.constexpr,
266
+ q_head_num: tl.constexpr,
267
+ BLOCK_DMODEL: tl.constexpr,
268
+ BLOCK_DPE: tl.constexpr,
269
+ BLOCK_DV: tl.constexpr,
270
+ BLOCK_N: tl.constexpr,
271
+ BLOCK_H: tl.constexpr,
272
+ MIN_BLOCK_KV: tl.constexpr,
273
+ logit_cap: tl.constexpr,
274
+ Lk: tl.constexpr,
275
+ Lv: tl.constexpr,
276
+ ):
277
+ cur_batch = tl.program_id(0)
278
+ cur_head_id = tl.program_id(1)
279
+ cur_kv_head = cur_head_id // tl.cdiv(kv_group_num, BLOCK_H)
280
+ split_kv_id = tl.program_id(2)
281
+
282
+ if BLOCK_H < kv_group_num:
283
+ VALID_BLOCK_H: tl.constexpr = BLOCK_H
284
+ else:
285
+ VALID_BLOCK_H: tl.constexpr = kv_group_num
286
+ cur_head = cur_head_id * VALID_BLOCK_H + tl.arange(0, BLOCK_H)
287
+ mask_h = cur_head < (cur_head_id + 1) * VALID_BLOCK_H
288
+ mask_h = mask_h & (cur_head < q_head_num)
289
+
290
+ offs_d = tl.arange(0, BLOCK_DMODEL)
291
+ offs_dv = tl.arange(0, BLOCK_DV)
292
+ mask_d = offs_d < Lk
293
+ mask_dv = offs_dv < Lv
294
+
295
+ cur_batch_kv_start_idx = tl.load(kv_indptr + cur_batch)
296
+ cur_batch_seq_len = tl.load(kv_indptr + cur_batch + 1) - cur_batch_kv_start_idx
297
+ kv_splits = tl.load(num_kv_splits + cur_batch)
298
+
299
+ offs_q = cur_batch * stride_qbs + cur_head[:, None] * stride_qh + offs_d[None, :]
300
+
301
+ if BLOCK_DPE > 0:
302
+ offs_dpe = BLOCK_DMODEL + tl.arange(0, BLOCK_DPE)
303
+ mask_dpe = offs_dpe < Lk
304
+ off_qpe = (
305
+ cur_batch * stride_qbs + cur_head[:, None] * stride_qh + offs_dpe[None, :]
306
+ )
307
+
308
+ kv_len_per_split = (
309
+ tl.cdiv(tl.cdiv(cur_batch_seq_len, kv_splits), MIN_BLOCK_KV) * MIN_BLOCK_KV
310
+ )
311
+ split_kv_start = kv_len_per_split * split_kv_id
312
+ split_kv_end = tl.minimum(split_kv_start + kv_len_per_split, cur_batch_seq_len)
313
+
314
+ e_max = tl.zeros([BLOCK_H], dtype=tl.float32) - float("inf")
315
+ e_sum = tl.zeros([BLOCK_H], dtype=tl.float32)
316
+ acc = tl.zeros([BLOCK_H, BLOCK_DV], dtype=tl.float32)
317
+
318
+ if split_kv_end > split_kv_start:
319
+ q = tl.load(Q + offs_q, mask=(mask_h[:, None]) & (mask_d[None, :]), other=0.0)
320
+ if BLOCK_DPE > 0:
321
+ qpe = tl.load(
322
+ Q + off_qpe, mask=(mask_h[:, None]) & (mask_dpe[None, :]), other=0.0
323
+ )
324
+ for start_n in range(split_kv_start, split_kv_end, BLOCK_N):
325
+ offs_n = start_n + tl.arange(0, BLOCK_N)
326
+ kv_loc = tl.load(
327
+ kv_indices + cur_batch_kv_start_idx + offs_n,
328
+ mask=offs_n < split_kv_end,
329
+ other=0,
330
+ )
331
+ offs_buf_k = (
332
+ kv_loc[None, :] * stride_buf_kbs
333
+ + cur_kv_head * stride_buf_kh
334
+ + offs_d[:, None]
335
+ )
336
+ k = tl.load(
337
+ K_Buffer + offs_buf_k,
338
+ mask=(offs_n[None, :] < split_kv_end) & (mask_d[:, None]),
339
+ other=0.0,
340
+ )
341
+ qk = tl.dot(q, k.to(q.dtype))
342
+ if BLOCK_DPE > 0:
343
+ offs_buf_kpe = (
344
+ kv_loc[None, :] * stride_buf_kbs
345
+ + cur_kv_head * stride_buf_kh
346
+ + offs_dpe[:, None]
347
+ )
348
+ kpe = tl.load(
349
+ K_Buffer + offs_buf_kpe,
350
+ mask=(offs_n[None, :] < split_kv_end) & (mask_dpe[:, None]),
351
+ other=0.0,
352
+ )
353
+ qk += tl.dot(qpe, kpe.to(qpe.dtype))
354
+ qk *= sm_scale
355
+
356
+ if logit_cap > 0:
357
+ qk = logit_cap * tanh(qk / logit_cap)
358
+
359
+ qk = tl.where(
360
+ mask_h[:, None] & (offs_n[None, :] < split_kv_end), qk, float("-inf")
361
+ )
362
+
363
+ offs_buf_v = (
364
+ kv_loc[:, None] * stride_buf_vbs
365
+ + cur_kv_head * stride_buf_vh
366
+ + offs_dv[None, :]
367
+ )
368
+ v = tl.load(
369
+ V_Buffer + offs_buf_v,
370
+ mask=(offs_n[:, None] < split_kv_end) & (mask_dv[None, :]),
371
+ other=0.0,
372
+ )
373
+
374
+ n_e_max = tl.maximum(tl.max(qk, 1), e_max)
375
+ re_scale = tl.exp(e_max - n_e_max)
376
+ p = tl.exp(qk - n_e_max[:, None])
377
+ acc *= re_scale[:, None]
378
+ acc += tl.dot(p.to(v.dtype), v)
379
+
380
+ e_sum = e_sum * re_scale + tl.sum(p, 1)
381
+ e_max = n_e_max
382
+
383
+ offs_mid_o = (
384
+ cur_batch * stride_mid_ob
385
+ + cur_head[:, None] * stride_mid_oh
386
+ + split_kv_id * stride_mid_os
387
+ + offs_dv[None, :]
388
+ )
389
+
390
+ tl.store(
391
+ Att_Out + offs_mid_o,
392
+ acc / e_sum[:, None],
393
+ mask=(mask_h[:, None]) & (mask_dv[None, :]),
394
+ )
395
+
396
+ offs_mid_o_1 = (
397
+ cur_batch * stride_mid_ob
398
+ + cur_head * stride_mid_oh
399
+ + split_kv_id * stride_mid_os
400
+ ) // Lv
401
+
402
+ tl.store(
403
+ Att_Lse + offs_mid_o_1,
404
+ e_max + tl.log(e_sum),
405
+ mask=mask_h,
406
+ )
407
+
408
+
409
+ def _decode_grouped_att_m_fwd(
410
+ q,
411
+ k_buffer,
412
+ v_buffer,
413
+ att_out,
414
+ att_lse,
415
+ kv_indptr,
416
+ kv_indices,
417
+ num_kv_splits,
418
+ max_kv_splits,
419
+ sm_scale,
420
+ logit_cap,
421
+ ):
422
+ BLOCK = 32
423
+ Lk = k_buffer.shape[-1]
424
+ Lv = v_buffer.shape[-1]
425
+
426
+ # [TODO] work around shmem limit on MI3xx
427
+ if _is_hip and Lk >= 576:
428
+ BLOCK = 16
429
+
430
+ if Lk == 576:
431
+ BLOCK_DMODEL = 512
432
+ BLOCK_DPE = 64
433
+ elif Lk == 288:
434
+ BLOCK_DMODEL = 256
435
+ BLOCK_DPE = 32
436
+ else:
437
+ BLOCK_DMODEL = triton.next_power_of_2(Lk)
438
+ BLOCK_DPE = 0
439
+ BLOCK_DV = triton.next_power_of_2(Lv)
440
+
441
+ batch, head_num = kv_indptr.shape[0] - 1, q.shape[1]
442
+ kv_group_num = q.shape[1] // k_buffer.shape[1]
443
+
444
+ BLOCK_H = 16
445
+ MAX_KV_SPLITS = max_kv_splits
446
+ grid = (
447
+ batch,
448
+ triton.cdiv(head_num, min(BLOCK_H, kv_group_num)),
449
+ MAX_KV_SPLITS,
450
+ )
451
+
452
+ extra_kargs = {}
453
+ num_stages = 2
454
+ if _is_hip:
455
+ # https://rocm.docs.amd.com/en/docs-6.2.0/how-to/llm-fine-tuning-optimization/optimizing-triton-kernel.html
456
+ # https://github.com/triton-lang/triton/blob/main/third_party/amd/backend/compiler.py
457
+ extra_kargs = {"waves_per_eu": 1, "matrix_instr_nonkdim": 16, "kpack": 2}
458
+ num_stages = 1
459
+
460
+ _fwd_grouped_kernel_stage1[grid](
461
+ q,
462
+ k_buffer,
463
+ v_buffer,
464
+ sm_scale,
465
+ kv_indptr,
466
+ kv_indices,
467
+ att_out,
468
+ att_lse,
469
+ num_kv_splits,
470
+ q.stride(0),
471
+ q.stride(1),
472
+ k_buffer.stride(0),
473
+ k_buffer.stride(1),
474
+ v_buffer.stride(0),
475
+ v_buffer.stride(1),
476
+ att_out.stride(0),
477
+ att_out.stride(1),
478
+ att_out.stride(2),
479
+ kv_group_num=kv_group_num,
480
+ q_head_num=head_num,
481
+ BLOCK_DMODEL=BLOCK_DMODEL,
482
+ BLOCK_DPE=BLOCK_DPE,
483
+ BLOCK_DV=BLOCK_DV,
484
+ BLOCK_N=BLOCK,
485
+ BLOCK_H=BLOCK_H,
486
+ MIN_BLOCK_KV=_MIN_BLOCK_KV,
487
+ logit_cap=logit_cap,
488
+ num_warps=4,
489
+ num_stages=num_stages,
490
+ Lk=Lk,
491
+ Lv=Lv,
492
+ **extra_kargs,
493
+ )
494
+
495
+
496
+ @triton.jit
497
+ def _fwd_kernel_stage2(
498
+ Mid_O,
499
+ Mid_O_1,
500
+ O,
501
+ kv_indptr,
502
+ num_kv_splits,
503
+ stride_mid_ob,
504
+ stride_mid_oh,
505
+ stride_mid_os,
506
+ stride_obs,
507
+ stride_oh,
508
+ MAX_KV_SPLITS: tl.constexpr,
509
+ MIN_BLOCK_KV: tl.constexpr,
510
+ BLOCK_DV: tl.constexpr,
511
+ Lv: tl.constexpr,
512
+ ):
513
+ cur_batch = tl.program_id(0)
514
+ cur_head = tl.program_id(1)
515
+
516
+ cur_batch_seq_len = tl.load(kv_indptr + cur_batch + 1) - tl.load(
517
+ kv_indptr + cur_batch
518
+ )
519
+ kv_splits = tl.load(num_kv_splits + cur_batch)
520
+
521
+ offs_d = tl.arange(0, BLOCK_DV)
522
+ mask_d = offs_d < Lv
523
+
524
+ e_sum = 0.0
525
+ e_max = -float("inf")
526
+ acc = tl.zeros([BLOCK_DV], dtype=tl.float32)
527
+
528
+ offs_v = cur_batch * stride_mid_ob + cur_head * stride_mid_oh + offs_d
529
+ offs_logic = (cur_batch * stride_mid_ob + cur_head * stride_mid_oh) // Lv
530
+ kv_len_per_split = (
531
+ tl.cdiv(tl.cdiv(cur_batch_seq_len, kv_splits), MIN_BLOCK_KV) * MIN_BLOCK_KV
532
+ )
533
+
534
+ for split_kv_id in range(0, MAX_KV_SPLITS):
535
+ split_kv_start = kv_len_per_split * split_kv_id
536
+ split_kv_end = tl.minimum(split_kv_start + kv_len_per_split, cur_batch_seq_len)
537
+
538
+ if split_kv_end > split_kv_start:
539
+ tv = tl.load(
540
+ Mid_O + offs_v + split_kv_id * stride_mid_os, mask=mask_d, other=0.0
541
+ )
542
+ tlogic = tl.load(Mid_O_1 + offs_logic + split_kv_id * stride_mid_os // Lv)
543
+ n_e_max = tl.maximum(tlogic, e_max)
544
+
545
+ old_scale = tl.exp(e_max - n_e_max)
546
+ acc *= old_scale
547
+ exp_logic = tl.exp(tlogic - n_e_max)
548
+ acc += exp_logic * tv
549
+
550
+ e_sum = e_sum * old_scale + exp_logic
551
+ e_max = n_e_max
552
+
553
+ tl.store(
554
+ O + cur_batch * stride_obs + cur_head * stride_oh + offs_d,
555
+ acc / e_sum,
556
+ mask=mask_d,
557
+ )
558
+
559
+
560
+ def _decode_softmax_reducev_fwd(
561
+ logits,
562
+ lse,
563
+ q,
564
+ o,
565
+ v_buffer,
566
+ kv_indptr,
567
+ num_kv_splits,
568
+ max_kv_splits,
569
+ ):
570
+ batch, head_num = q.shape[0], q.shape[1]
571
+ Lv = v_buffer.shape[-1]
572
+ BLOCK_DV = triton.next_power_of_2(Lv)
573
+
574
+ MAX_KV_SPLITS = max_kv_splits
575
+
576
+ extra_kargs = {}
577
+ if _is_hip:
578
+ # https://rocm.docs.amd.com/en/docs-6.2.0/how-to/llm-fine-tuning-optimization/optimizing-triton-kernel.html
579
+ # https://github.com/triton-lang/triton/blob/main/third_party/amd/backend/compiler.py
580
+ extra_kargs = {"waves_per_eu": 4, "matrix_instr_nonkdim": 16, "kpack": 2}
581
+
582
+ grid = (batch, head_num)
583
+ _fwd_kernel_stage2[grid](
584
+ logits,
585
+ lse,
586
+ o,
587
+ kv_indptr,
588
+ num_kv_splits,
589
+ logits.stride(0),
590
+ logits.stride(1),
591
+ logits.stride(2),
592
+ o.stride(0),
593
+ o.stride(1),
594
+ MAX_KV_SPLITS=MAX_KV_SPLITS,
595
+ MIN_BLOCK_KV=_MIN_BLOCK_KV,
596
+ BLOCK_DV=BLOCK_DV,
597
+ Lv=Lv,
598
+ num_warps=4,
599
+ num_stages=2,
600
+ **extra_kargs,
601
+ )
602
+
603
+
604
+ def decode_attention_fwd_normal(
605
+ q,
606
+ k_buffer,
607
+ v_buffer,
608
+ o,
609
+ kv_indptr,
610
+ kv_indices,
611
+ attn_logits,
612
+ attn_lse,
613
+ num_kv_splits,
614
+ max_kv_splits,
615
+ sm_scale,
616
+ logit_cap=0.0,
617
+ ):
618
+ _decode_att_m_fwd(
619
+ q,
620
+ k_buffer,
621
+ v_buffer,
622
+ attn_logits,
623
+ attn_lse,
624
+ kv_indptr,
625
+ kv_indices,
626
+ num_kv_splits,
627
+ max_kv_splits,
628
+ sm_scale,
629
+ logit_cap,
630
+ )
631
+ _decode_softmax_reducev_fwd(
632
+ attn_logits,
633
+ attn_lse,
634
+ q,
635
+ o,
636
+ v_buffer,
637
+ kv_indptr,
638
+ num_kv_splits,
639
+ max_kv_splits,
640
+ )
641
+
642
+
643
+ def decode_attention_fwd_grouped(
644
+ q,
645
+ k_buffer,
646
+ v_buffer,
647
+ o,
648
+ kv_indptr,
649
+ kv_indices,
650
+ attn_logits,
651
+ attn_lse,
652
+ num_kv_splits,
653
+ max_kv_splits,
654
+ sm_scale,
655
+ logit_cap=0.0,
656
+ ):
657
+ _decode_grouped_att_m_fwd(
658
+ q,
659
+ k_buffer,
660
+ v_buffer,
661
+ attn_logits,
662
+ attn_lse,
663
+ kv_indptr,
664
+ kv_indices,
665
+ num_kv_splits,
666
+ max_kv_splits,
667
+ sm_scale,
668
+ logit_cap,
669
+ )
670
+ _decode_softmax_reducev_fwd(
671
+ attn_logits,
672
+ attn_lse,
673
+ q,
674
+ o,
675
+ v_buffer,
676
+ kv_indptr,
677
+ num_kv_splits,
678
+ max_kv_splits,
679
+ )
680
+
681
+
682
+ def decode_attention_fwd(
683
+ q,
684
+ k_buffer,
685
+ v_buffer,
686
+ o,
687
+ kv_indptr,
688
+ kv_indices,
689
+ attn_logits,
690
+ attn_lse,
691
+ num_kv_splits,
692
+ max_kv_splits,
693
+ sm_scale,
694
+ logit_cap=0.0,
695
+ ):
696
+ assert max_kv_splits == attn_logits.shape[2]
697
+ assert q.shape[0] <= kv_indptr.shape[0] - 1
698
+ assert q.shape[0] <= attn_logits.shape[0]
699
+
700
+ kv_group_num = q.shape[1] // v_buffer.shape[1]
701
+
702
+ if kv_group_num == 1:
703
+ # MHA
704
+ decode_attention_fwd_normal(
705
+ q,
706
+ k_buffer,
707
+ v_buffer,
708
+ o,
709
+ kv_indptr,
710
+ kv_indices,
711
+ attn_logits,
712
+ attn_lse,
713
+ num_kv_splits,
714
+ max_kv_splits,
715
+ sm_scale,
716
+ logit_cap,
717
+ )
718
+ else:
719
+ # GQA/MQA/MLA
720
+ decode_attention_fwd_grouped(
721
+ q,
722
+ k_buffer,
723
+ v_buffer,
724
+ o,
725
+ kv_indptr,
726
+ kv_indices,
727
+ attn_logits,
728
+ attn_lse,
729
+ num_kv_splits,
730
+ max_kv_splits,
731
+ sm_scale,
732
+ logit_cap,
733
+ )
lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/double_sparsity_attention.py ADDED
@@ -0,0 +1,1106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import triton
3
+ import triton.language as tl
4
+
5
+ from sglang.srt.managers.schedule_batch import global_server_args_dict
6
+ from sglang.srt.utils import is_cuda, is_hip
7
+
8
+ _is_cuda = is_cuda()
9
+ if _is_cuda:
10
+ CUDA_CAPABILITY = torch.cuda.get_device_capability()
11
+
12
+ _is_hip = is_hip()
13
+
14
+ if global_server_args_dict.get("attention_reduce_in_fp32", False):
15
+ REDUCE_TRITON_TYPE = tl.float32
16
+ REDUCE_TORCH_TYPE = torch.float32
17
+ else:
18
+ REDUCE_TRITON_TYPE = tl.float16
19
+ REDUCE_TORCH_TYPE = torch.float16
20
+
21
+
22
+ @triton.jit
23
+ def tanh(x):
24
+ # Tanh is just a scaled sigmoid
25
+ return 2 * tl.sigmoid(2 * x) - 1
26
+
27
+
28
+ @triton.jit
29
+ def _fwd_kernel_flash_decode_stage1(
30
+ Q,
31
+ K,
32
+ V,
33
+ sm_scale,
34
+ Req_to_tokens,
35
+ B_req_idx,
36
+ B_Seqlen,
37
+ Mid_O, # [batch, head, seq_block_num, head_dim]
38
+ Mid_O_LogExpSum, # [batch, head, seq_block_num]
39
+ stride_req_to_tokens_b,
40
+ stride_req_to_tokens_s,
41
+ stride_qbs,
42
+ stride_qh,
43
+ stride_qd,
44
+ stride_kbs,
45
+ stride_kh,
46
+ stride_kd,
47
+ stride_vbs,
48
+ stride_vh,
49
+ stride_vd,
50
+ stride_mid_ob,
51
+ stride_mid_oh,
52
+ stride_mid_os,
53
+ stride_mid_od,
54
+ stride_mid_o_eb,
55
+ stride_mid_o_eh,
56
+ stride_mid_o_es,
57
+ gqa_group_size,
58
+ BLOCK_SEQ: tl.constexpr,
59
+ BLOCK_DMODEL: tl.constexpr,
60
+ BLOCK_N: tl.constexpr,
61
+ ):
62
+ cur_batch = tl.program_id(0)
63
+ cur_head = tl.program_id(1)
64
+ seq_start_block = tl.program_id(2)
65
+ cur_kv_head = cur_head // gqa_group_size
66
+
67
+ offs_d = tl.arange(0, BLOCK_DMODEL)
68
+ cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
69
+ cur_batch_req_idx = tl.load(B_req_idx + cur_batch)
70
+ cur_batch_start_index = seq_start_block * BLOCK_SEQ
71
+ cur_batch_end_index = tl.minimum(
72
+ cur_batch_seq_len, cur_batch_start_index + BLOCK_SEQ
73
+ )
74
+
75
+ off_q = cur_batch * stride_qbs + cur_head * stride_qh + offs_d
76
+
77
+ block_n_size = (
78
+ tl.where(
79
+ cur_batch_end_index - cur_batch_start_index <= 0,
80
+ 0,
81
+ cur_batch_end_index - cur_batch_start_index + BLOCK_N - 1,
82
+ )
83
+ // BLOCK_N
84
+ )
85
+
86
+ offs_n = cur_batch_start_index + tl.arange(0, BLOCK_N)
87
+
88
+ q = tl.load(Q + off_q)
89
+
90
+ sum_exp = 0.0
91
+ max_logic = -float("inf")
92
+ acc = tl.zeros([BLOCK_DMODEL], dtype=tl.float32)
93
+
94
+ for start_n in range(0, block_n_size, 1):
95
+ offs_n_new = start_n * BLOCK_N + offs_n
96
+ k_loc = tl.load(
97
+ Req_to_tokens + stride_req_to_tokens_b * cur_batch_req_idx + offs_n_new,
98
+ mask=offs_n_new < cur_batch_end_index,
99
+ other=0,
100
+ )
101
+ off_k = k_loc[:, None] * stride_kbs + cur_kv_head * stride_kh + offs_d[None, :]
102
+ k = tl.load(
103
+ K + off_k, mask=offs_n_new[:, None] < cur_batch_end_index, other=0.0
104
+ )
105
+ att_value = tl.sum(q[None, :] * k, 1)
106
+ att_value *= sm_scale
107
+ att_value = tl.where(offs_n_new < cur_batch_end_index, att_value, float("-inf"))
108
+ v = tl.load(
109
+ V + off_k, mask=offs_n_new[:, None] < cur_batch_end_index, other=0.0
110
+ )
111
+
112
+ cur_max_logic = tl.max(att_value, axis=0)
113
+ new_max_logic = tl.maximum(cur_max_logic, max_logic)
114
+
115
+ exp_logic = tl.exp(att_value - new_max_logic)
116
+ logic_scale = tl.exp(max_logic - new_max_logic)
117
+ acc *= logic_scale
118
+ acc += tl.sum(exp_logic[:, None] * v, axis=0)
119
+
120
+ sum_exp = sum_exp * logic_scale + tl.sum(exp_logic, axis=0)
121
+ max_logic = new_max_logic
122
+
123
+ need_store = tl.where(block_n_size == 0, 0, 1)
124
+ for _ in range(0, need_store, 1):
125
+ off_mid_o = (
126
+ cur_batch * stride_mid_ob
127
+ + cur_head * stride_mid_oh
128
+ + seq_start_block * stride_mid_os
129
+ + offs_d
130
+ )
131
+ off_mid_o_logexpsum = (
132
+ cur_batch * stride_mid_o_eb + cur_head * stride_mid_o_eh + seq_start_block
133
+ )
134
+ tl.store(Mid_O + off_mid_o, acc / sum_exp)
135
+ tl.store(Mid_O_LogExpSum + off_mid_o_logexpsum, max_logic + tl.log(sum_exp))
136
+ return
137
+
138
+
139
+ @triton.jit
140
+ def _fwd_kernel_flash_decode_stage2(
141
+ B_Seqlen,
142
+ Mid_O, # [batch, head, seq_block_num, head_dim]
143
+ Mid_O_LogExpSum, # [batch, head, seq_block_num]
144
+ O, # [batch, head, head_dim]
145
+ stride_mid_ob,
146
+ stride_mid_oh,
147
+ stride_mid_os,
148
+ stride_mid_od,
149
+ stride_mid_o_eb,
150
+ stride_mid_o_eh,
151
+ stride_mid_o_es,
152
+ stride_obs,
153
+ stride_oh,
154
+ stride_od,
155
+ BLOCK_SEQ: tl.constexpr,
156
+ BLOCK_DMODEL: tl.constexpr,
157
+ ):
158
+ cur_batch = tl.program_id(0)
159
+ cur_head = tl.program_id(1)
160
+
161
+ offs_d = tl.arange(0, BLOCK_DMODEL)
162
+ cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
163
+
164
+ block_n_size = (
165
+ tl.where(cur_batch_seq_len <= 0, 0, cur_batch_seq_len + BLOCK_SEQ - 1)
166
+ // BLOCK_SEQ
167
+ )
168
+
169
+ sum_exp = 0.0
170
+ max_logic = -float("inf")
171
+ acc = tl.zeros([BLOCK_DMODEL], dtype=tl.float32)
172
+
173
+ offs_v = cur_batch * stride_mid_ob + cur_head * stride_mid_oh + offs_d
174
+ offs_logic = cur_batch * stride_mid_o_eb + cur_head * stride_mid_o_eh
175
+ for block_seq_n in range(0, block_n_size, 1):
176
+ tv = tl.load(Mid_O + offs_v + block_seq_n * stride_mid_os)
177
+ tlogic = tl.load(Mid_O_LogExpSum + offs_logic + block_seq_n)
178
+ new_max_logic = tl.maximum(tlogic, max_logic)
179
+
180
+ old_scale = tl.exp(max_logic - new_max_logic)
181
+ acc *= old_scale
182
+ exp_logic = tl.exp(tlogic - new_max_logic)
183
+ acc += exp_logic * tv
184
+ sum_exp = sum_exp * old_scale + exp_logic
185
+ max_logic = new_max_logic
186
+
187
+ tl.store(O + cur_batch * stride_obs + cur_head * stride_oh + offs_d, acc / sum_exp)
188
+ return
189
+
190
+
191
+ @torch.no_grad()
192
+ def flash_decode_stage1(
193
+ q,
194
+ k,
195
+ v,
196
+ Req_to_tokens,
197
+ B_req_idx,
198
+ B_Seqlen,
199
+ max_len_in_batch,
200
+ mid_out,
201
+ mid_out_logsumexp,
202
+ block_seq,
203
+ ):
204
+ BLOCK_SEQ = block_seq
205
+ BLOCK_N = 16
206
+ assert BLOCK_SEQ % BLOCK_N == 0
207
+ # shape constraints
208
+ Lq, Lk = q.shape[-1], k.shape[-1]
209
+ assert Lq == Lk
210
+ assert Lk in {16, 32, 64, 128}
211
+ sm_scale = 1.0 / (Lk**0.5)
212
+ batch, head_num = B_req_idx.shape[0], q.shape[1]
213
+ grid = (batch, head_num, triton.cdiv(max_len_in_batch, BLOCK_SEQ))
214
+ gqa_group_size = q.shape[1] // k.shape[1]
215
+
216
+ _fwd_kernel_flash_decode_stage1[grid](
217
+ q,
218
+ k,
219
+ v,
220
+ sm_scale,
221
+ Req_to_tokens,
222
+ B_req_idx,
223
+ B_Seqlen,
224
+ mid_out,
225
+ mid_out_logsumexp,
226
+ Req_to_tokens.stride(0),
227
+ Req_to_tokens.stride(1),
228
+ q.stride(0),
229
+ q.stride(1),
230
+ q.stride(2),
231
+ k.stride(0),
232
+ k.stride(1),
233
+ k.stride(2),
234
+ v.stride(0),
235
+ v.stride(1),
236
+ v.stride(2),
237
+ mid_out.stride(0),
238
+ mid_out.stride(1),
239
+ mid_out.stride(2),
240
+ mid_out.stride(3),
241
+ mid_out_logsumexp.stride(0),
242
+ mid_out_logsumexp.stride(1),
243
+ mid_out_logsumexp.stride(2),
244
+ gqa_group_size,
245
+ BLOCK_SEQ=BLOCK_SEQ,
246
+ BLOCK_DMODEL=Lk,
247
+ BLOCK_N=BLOCK_N,
248
+ num_warps=1,
249
+ num_stages=2,
250
+ )
251
+ return
252
+
253
+
254
+ @torch.no_grad()
255
+ def flash_decode_stage2(mid_out, mid_out_logexpsum, B_Seqlen, O, block_seq):
256
+ Lk = mid_out.shape[-1]
257
+ assert Lk in {16, 32, 64, 128}
258
+ batch, head_num = mid_out.shape[0], mid_out.shape[1]
259
+ grid = (batch, head_num)
260
+
261
+ _fwd_kernel_flash_decode_stage2[grid](
262
+ B_Seqlen,
263
+ mid_out,
264
+ mid_out_logexpsum,
265
+ O,
266
+ mid_out.stride(0),
267
+ mid_out.stride(1),
268
+ mid_out.stride(2),
269
+ mid_out.stride(3),
270
+ mid_out_logexpsum.stride(0),
271
+ mid_out_logexpsum.stride(1),
272
+ mid_out_logexpsum.stride(2),
273
+ O.stride(0),
274
+ O.stride(1),
275
+ O.stride(2),
276
+ BLOCK_SEQ=block_seq,
277
+ BLOCK_DMODEL=Lk,
278
+ num_warps=4,
279
+ num_stages=2,
280
+ )
281
+ return
282
+
283
+
284
+ def flash_decode_attention_fwd(
285
+ q,
286
+ k_buffer,
287
+ v_buffer,
288
+ o,
289
+ req_to_token,
290
+ b_req_idx,
291
+ b_start_loc,
292
+ b_seq_len,
293
+ attn_logits,
294
+ max_len_in_batch,
295
+ sm_scale,
296
+ logit_cap=0.0,
297
+ ):
298
+ BLOCK_SEQ = 256
299
+ kv_group_num = q.shape[1] // v_buffer.shape[1]
300
+ # batch_size = q.shape[0]
301
+
302
+ block_seq_num = (max_len_in_batch + BLOCK_SEQ - 1) // BLOCK_SEQ
303
+
304
+ mid_o = torch.empty(
305
+ [q.shape[0], q.shape[1], block_seq_num, q.shape[-1]],
306
+ dtype=torch.float32,
307
+ device="cuda",
308
+ )
309
+ mid_o_logexpsum = torch.empty(
310
+ [q.shape[0], q.shape[1], block_seq_num], dtype=torch.float32, device="cuda"
311
+ )
312
+
313
+ flash_decode_stage1(
314
+ q,
315
+ k_buffer,
316
+ v_buffer,
317
+ req_to_token,
318
+ b_req_idx,
319
+ b_seq_len,
320
+ max_len_in_batch,
321
+ mid_o,
322
+ mid_o_logexpsum,
323
+ BLOCK_SEQ,
324
+ )
325
+ flash_decode_stage2(mid_o, mid_o_logexpsum, b_seq_len, o, BLOCK_SEQ)
326
+
327
+
328
+ @triton.jit
329
+ def _sparse_fwd_kernel_flash_decode_stage1( # Double Sparsity's approximate attention
330
+ Q_Label,
331
+ K_Label_Buffer,
332
+ sm_scale,
333
+ Req_to_tokens, # shape: [B, S]
334
+ B_Seqlen,
335
+ Att_Out, # shape: [H, B, S] easier for topk
336
+ stride_req_to_tokens_b,
337
+ stride_qbs,
338
+ stride_qh,
339
+ stride_buf_kbs,
340
+ stride_buf_kh,
341
+ att_stride_h,
342
+ att_stride_b,
343
+ kv_group_num: tl.constexpr,
344
+ BLOCK_DMODEL: tl.constexpr,
345
+ BLOCK_N: tl.constexpr,
346
+ logit_cap: tl.constexpr,
347
+ ):
348
+ cur_batch = tl.program_id(0)
349
+ cur_head = tl.program_id(1)
350
+ start_n = tl.program_id(2)
351
+
352
+ cur_kv_head = cur_head // kv_group_num
353
+
354
+ offs_d = tl.arange(0, BLOCK_DMODEL)
355
+ cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
356
+
357
+ cur_batch_start_index = 0
358
+ cur_batch_end_index = cur_batch_seq_len
359
+
360
+ min_val = -float("inf")
361
+ att_value = tl.full([BLOCK_N], min_val, dtype=tl.float32)
362
+
363
+ off_q = cur_batch * stride_qbs + cur_head * stride_qh + offs_d
364
+
365
+ offs_n = start_n * BLOCK_N + tl.arange(0, BLOCK_N)
366
+
367
+ block_index = start_n * BLOCK_N
368
+ block_mask = tl.where(block_index < cur_batch_seq_len, 1, 0)
369
+
370
+ for start_mark in range(0, block_mask, 1):
371
+ q = tl.load(Q_Label + off_q + start_mark).to(REDUCE_TRITON_TYPE)
372
+ offs_n_new = cur_batch_start_index + offs_n
373
+ k_loc = tl.load(
374
+ Req_to_tokens + stride_req_to_tokens_b * cur_batch + offs_n_new,
375
+ mask=offs_n_new < cur_batch_end_index,
376
+ other=0,
377
+ )
378
+ offs_buf_k = (
379
+ k_loc[:, None] * stride_buf_kbs
380
+ + cur_kv_head * stride_buf_kh
381
+ + offs_d[None, :]
382
+ )
383
+ k = tl.load(
384
+ K_Label_Buffer + offs_buf_k,
385
+ mask=offs_n_new[:, None] < cur_batch_end_index,
386
+ other=0.0,
387
+ ).to(REDUCE_TRITON_TYPE)
388
+
389
+ att_value = tl.sum(q[None, :] * k, 1)
390
+ att_value *= sm_scale
391
+
392
+ if logit_cap > 0:
393
+ att_value = logit_cap * tanh(att_value / logit_cap)
394
+
395
+ att_value = tl.where(offs_n < cur_batch_end_index, att_value, min_val)
396
+ off_o = cur_head * att_stride_h + (cur_batch * att_stride_b + offs_n)
397
+ tl.store(Att_Out + off_o, att_value)
398
+
399
+
400
+ @triton.jit
401
+ def _sparse_fwd_kernel_flash_decode_stage2(
402
+ Q,
403
+ K,
404
+ V,
405
+ sm_scale,
406
+ Req_to_tokens, # shape: [B, S]
407
+ Topk_token_indices, # shape: [H, B, k]
408
+ Mid_O, # [batch, head, seq_block_num, head_dim]
409
+ Mid_O_LogExpSum, # [batch, head, seq_block_num]
410
+ Heavy_token_num, # NOTE: This can be used as constexpr but we may support dynamic heavy token number in the future
411
+ stride_req_to_tokens_b,
412
+ stride_topk_token_indices_h,
413
+ stride_topk_token_indices_b,
414
+ stride_qbs,
415
+ stride_qh,
416
+ stride_kbs,
417
+ stride_kh,
418
+ stride_vbs,
419
+ stride_vh,
420
+ stride_mid_ob,
421
+ stride_mid_oh,
422
+ stride_mid_os,
423
+ stride_mid_o_eb,
424
+ stride_mid_o_eh,
425
+ gqa_group_size,
426
+ BLOCK_SEQ: tl.constexpr,
427
+ BLOCK_DMODEL: tl.constexpr,
428
+ BLOCK_N: tl.constexpr,
429
+ ):
430
+ cur_batch = tl.program_id(0)
431
+ cur_head = tl.program_id(1)
432
+ seq_start_block = tl.program_id(2)
433
+ cur_kv_head = cur_head // gqa_group_size
434
+
435
+ offs_d = tl.arange(0, BLOCK_DMODEL)
436
+ cur_batch_start_index = seq_start_block * BLOCK_SEQ
437
+ cur_batch_end_index = tl.minimum(Heavy_token_num, cur_batch_start_index + BLOCK_SEQ)
438
+
439
+ off_q = cur_batch * stride_qbs + cur_head * stride_qh + offs_d
440
+
441
+ block_n_size = (
442
+ tl.where(
443
+ cur_batch_end_index - cur_batch_start_index <= 0,
444
+ 0,
445
+ cur_batch_end_index - cur_batch_start_index + BLOCK_N - 1,
446
+ )
447
+ // BLOCK_N
448
+ )
449
+
450
+ # offs_n = cur_batch_start_index + tl.arange(0, BLOCK_N)
451
+ offs_n = tl.arange(0, BLOCK_N)
452
+
453
+ q = tl.load(Q + off_q)
454
+
455
+ sum_exp = 0.0
456
+ max_logic = -float("inf")
457
+ acc = tl.zeros([BLOCK_DMODEL], dtype=tl.float32)
458
+
459
+ for start_n in range(cur_batch_start_index, cur_batch_end_index, BLOCK_N):
460
+ # for start_n in range(0, block_n_size, 1):
461
+ # offs_n_new = start_n * BLOCK_N + offs_n
462
+ offs_n_new = start_n + offs_n
463
+ # offs_n_new = cur_batch_start_index + start_n * BLOCK_N + offs_n
464
+ topk_token_indices = tl.load(
465
+ Topk_token_indices
466
+ + stride_topk_token_indices_h * cur_head
467
+ + stride_topk_token_indices_b * cur_batch
468
+ + offs_n_new,
469
+ mask=offs_n_new < cur_batch_end_index,
470
+ other=0,
471
+ )
472
+ k_loc = tl.load(
473
+ Req_to_tokens + stride_req_to_tokens_b * cur_batch + topk_token_indices,
474
+ mask=offs_n_new < cur_batch_end_index,
475
+ other=0,
476
+ )
477
+ off_k = k_loc[:, None] * stride_kbs + cur_kv_head * stride_kh + offs_d[None, :]
478
+ k = tl.load(
479
+ K + off_k, mask=offs_n_new[:, None] < cur_batch_end_index, other=0.0
480
+ )
481
+ att_value = tl.sum(q[None, :] * k, 1)
482
+ att_value *= sm_scale
483
+ att_value = tl.where(offs_n_new < cur_batch_end_index, att_value, float("-inf"))
484
+ v = tl.load(
485
+ V + off_k, mask=offs_n_new[:, None] < cur_batch_end_index, other=0.0
486
+ )
487
+
488
+ cur_max_logic = tl.max(att_value, axis=0)
489
+ new_max_logic = tl.maximum(cur_max_logic, max_logic)
490
+
491
+ exp_logic = tl.exp(att_value - new_max_logic)
492
+ logic_scale = tl.exp(max_logic - new_max_logic)
493
+ acc *= logic_scale
494
+ acc += tl.sum(exp_logic[:, None] * v, axis=0)
495
+
496
+ sum_exp = sum_exp * logic_scale + tl.sum(exp_logic, axis=0)
497
+ max_logic = new_max_logic
498
+
499
+ # need_store = tl.where(block_n_size == 0, 0, 1)
500
+ need_store = 1
501
+ for _ in range(0, need_store, 1):
502
+ off_mid_o = (
503
+ cur_batch * stride_mid_ob
504
+ + cur_head * stride_mid_oh
505
+ + seq_start_block * stride_mid_os
506
+ + offs_d
507
+ )
508
+ off_mid_o_logexpsum = (
509
+ cur_batch * stride_mid_o_eb + cur_head * stride_mid_o_eh + seq_start_block
510
+ )
511
+ tl.store(Mid_O + off_mid_o, acc / sum_exp)
512
+ tl.store(Mid_O_LogExpSum + off_mid_o_logexpsum, max_logic + tl.log(sum_exp))
513
+ return
514
+
515
+
516
+ @triton.jit
517
+ def _sparse_fwd_kernel_flash_decode_stage3(
518
+ Mid_O, # [batch, head, seq_block_num, head_dim]
519
+ Mid_O_LogExpSum, # [batch, head, seq_block_num]
520
+ O, # [batch, head, head_dim]
521
+ seq_len, # NOTE: This can be used as constexpr but we may support dynamic heavy token number in the future
522
+ stride_mid_ob,
523
+ stride_mid_oh,
524
+ stride_mid_os,
525
+ stride_mid_o_eb,
526
+ stride_mid_o_eh,
527
+ stride_obs,
528
+ stride_oh,
529
+ BLOCK_SEQ: tl.constexpr,
530
+ BLOCK_DMODEL: tl.constexpr,
531
+ ):
532
+ cur_batch = tl.program_id(0)
533
+ cur_head = tl.program_id(1)
534
+
535
+ offs_d = tl.arange(0, BLOCK_DMODEL)
536
+
537
+ block_n_size = tl.where(seq_len <= 0, 0, seq_len + BLOCK_SEQ - 1) // BLOCK_SEQ
538
+
539
+ sum_exp = 0.0
540
+ max_logic = -float("inf")
541
+ acc = tl.zeros([BLOCK_DMODEL], dtype=tl.float32)
542
+
543
+ offs_v = cur_batch * stride_mid_ob + cur_head * stride_mid_oh + offs_d
544
+ offs_logic = cur_batch * stride_mid_o_eb + cur_head * stride_mid_o_eh
545
+ for block_seq_n in range(0, block_n_size, 1):
546
+ tv = tl.load(Mid_O + offs_v + block_seq_n * stride_mid_os)
547
+ tlogic = tl.load(Mid_O_LogExpSum + offs_logic + block_seq_n)
548
+ new_max_logic = tl.maximum(tlogic, max_logic)
549
+
550
+ old_scale = tl.exp(max_logic - new_max_logic)
551
+ acc *= old_scale
552
+ exp_logic = tl.exp(tlogic - new_max_logic)
553
+ acc += exp_logic * tv
554
+ sum_exp = sum_exp * old_scale + exp_logic
555
+ max_logic = new_max_logic
556
+
557
+ tl.store(O + cur_batch * stride_obs + cur_head * stride_oh + offs_d, acc / sum_exp)
558
+ return
559
+
560
+
561
+ def sparse_flash_decode_stage1(
562
+ q_label,
563
+ k_label_buffer,
564
+ att_out,
565
+ Req_to_tokens,
566
+ B_Seqlen,
567
+ max_len_in_batch,
568
+ sm_scale,
569
+ logit_cap,
570
+ ):
571
+ BLOCK = 32
572
+ # shape constraints
573
+ Lq, Lk = q_label.shape[-1], k_label_buffer.shape[-1]
574
+ assert Lq == Lk
575
+ assert Lk in {16, 32, 64, 128, 256, 576}
576
+
577
+ BLOCK_DMODEL = Lk
578
+
579
+ batch, head_num = q_label.shape[0], q_label.shape[1]
580
+
581
+ grid = (batch, head_num, triton.cdiv(max_len_in_batch, BLOCK))
582
+ kv_group_num = q_label.shape[1] // k_label_buffer.shape[1]
583
+
584
+ if kv_group_num == 1:
585
+ num_warps = 4
586
+ else:
587
+ num_warps = 2
588
+
589
+ _sparse_fwd_kernel_flash_decode_stage1[grid](
590
+ q_label,
591
+ k_label_buffer,
592
+ sm_scale,
593
+ Req_to_tokens,
594
+ B_Seqlen,
595
+ att_out,
596
+ Req_to_tokens.stride(0),
597
+ q_label.stride(0),
598
+ q_label.stride(1),
599
+ k_label_buffer.stride(0),
600
+ k_label_buffer.stride(1),
601
+ att_out.stride(0),
602
+ att_out.stride(1),
603
+ kv_group_num,
604
+ BLOCK_DMODEL,
605
+ BLOCK,
606
+ logit_cap,
607
+ num_warps=num_warps,
608
+ num_stages=1,
609
+ )
610
+
611
+
612
+ @torch.no_grad()
613
+ def sparse_flash_decode_stage2(
614
+ q,
615
+ k,
616
+ v,
617
+ Req_to_tokens,
618
+ Topk_token_indices,
619
+ heavy_token_num,
620
+ mid_out,
621
+ mid_out_logsumexp,
622
+ block_seq,
623
+ sm_scale,
624
+ ):
625
+ BLOCK_SEQ = block_seq
626
+ BLOCK_N = 16
627
+ assert BLOCK_SEQ % BLOCK_N == 0
628
+ # shape constraints
629
+ Lq, Lk = q.shape[-1], k.shape[-1]
630
+ assert Lq == Lk
631
+ assert Lk in {16, 32, 64, 128}
632
+ assert heavy_token_num == Topk_token_indices.shape[-1]
633
+ # sm_scale = 1.0 / (Lk ** 0.5)
634
+ batch, head_num = q.shape[0], q.shape[1]
635
+ grid = (batch, head_num, triton.cdiv(heavy_token_num, BLOCK_SEQ))
636
+
637
+ gqa_group_size = q.shape[1] // k.shape[1]
638
+
639
+ _sparse_fwd_kernel_flash_decode_stage2[grid](
640
+ q,
641
+ k,
642
+ v,
643
+ sm_scale,
644
+ Req_to_tokens,
645
+ Topk_token_indices,
646
+ mid_out,
647
+ mid_out_logsumexp,
648
+ heavy_token_num,
649
+ Req_to_tokens.stride(0),
650
+ Topk_token_indices.stride(0),
651
+ Topk_token_indices.stride(1),
652
+ q.stride(0),
653
+ q.stride(1),
654
+ k.stride(0),
655
+ k.stride(1),
656
+ v.stride(0),
657
+ v.stride(1),
658
+ mid_out.stride(0),
659
+ mid_out.stride(1),
660
+ mid_out.stride(2),
661
+ mid_out_logsumexp.stride(0),
662
+ mid_out_logsumexp.stride(1),
663
+ gqa_group_size,
664
+ BLOCK_SEQ=BLOCK_SEQ,
665
+ BLOCK_DMODEL=Lk,
666
+ BLOCK_N=BLOCK_N,
667
+ num_warps=1,
668
+ num_stages=2,
669
+ )
670
+ return
671
+
672
+
673
+ @torch.no_grad()
674
+ def sparse_flash_decode_stage3(Seqlen, mid_out, mid_out_logexpsum, O, block_seq):
675
+ Lk = mid_out.shape[-1]
676
+ assert Lk in {16, 32, 64, 128}
677
+ batch, head_num = mid_out.shape[0], mid_out.shape[1]
678
+ grid = (batch, head_num)
679
+
680
+ _sparse_fwd_kernel_flash_decode_stage3[grid](
681
+ mid_out,
682
+ mid_out_logexpsum,
683
+ O,
684
+ Seqlen,
685
+ mid_out.stride(0),
686
+ mid_out.stride(1),
687
+ mid_out.stride(2),
688
+ mid_out_logexpsum.stride(0),
689
+ mid_out_logexpsum.stride(1),
690
+ O.stride(0),
691
+ O.stride(1),
692
+ BLOCK_SEQ=block_seq,
693
+ BLOCK_DMODEL=Lk,
694
+ num_warps=4,
695
+ num_stages=2,
696
+ )
697
+ return
698
+
699
+
700
+ def flash_decode_sparse_attention_fwd(
701
+ q,
702
+ k_buffer,
703
+ v_buffer,
704
+ o,
705
+ q_label,
706
+ k_label_buffer,
707
+ req_to_token,
708
+ b_seq_len,
709
+ max_len_in_batch,
710
+ sm_scale,
711
+ logit_cap,
712
+ heavy_token_num=32,
713
+ att_out_approx=None,
714
+ mid_out=None,
715
+ mid_o_logexpsum=None,
716
+ BLOCK_SEQ=256,
717
+ ):
718
+ # TODO(Andy): Tune BLOCK_SEQ & BLOCK_D
719
+ kv_group_num = q.shape[1] // v_buffer.shape[1]
720
+ # batch_size = q.shape[0]
721
+
722
+ # Step 1: BGEMV approximate attention (page implementation)
723
+
724
+ if att_out_approx is None:
725
+ att_out_approx = torch.empty(
726
+ [q.shape[1], q.shape[0], max_len_in_batch],
727
+ dtype=REDUCE_TORCH_TYPE,
728
+ device=q.device,
729
+ )
730
+
731
+ if mid_out is None:
732
+ block_seq_num = (heavy_token_num + BLOCK_SEQ - 1) // BLOCK_SEQ
733
+
734
+ mid_out = torch.empty(
735
+ [q.shape[0], q.shape[1], block_seq_num, q.shape[-1]],
736
+ dtype=torch.float32,
737
+ device=q.device,
738
+ )
739
+ mid_o_logexpsum = torch.empty(
740
+ [q.shape[0], q.shape[1], block_seq_num],
741
+ dtype=torch.float32,
742
+ device=q.device,
743
+ )
744
+
745
+ sparse_flash_decode_stage1(
746
+ q_label,
747
+ k_label_buffer,
748
+ att_out_approx,
749
+ req_to_token,
750
+ b_seq_len,
751
+ max_len_in_batch,
752
+ sm_scale,
753
+ logit_cap,
754
+ )
755
+
756
+ # Step 2: TopK token selection
757
+ # NOTE(Andy): Apply sparse decoding when min > heavy_token_num and max > sparse decoding threshold
758
+ # TODO(Andy): Change a faster topk implementation
759
+ topk_token_indices = torch.topk(att_out_approx, heavy_token_num, dim=-1).indices
760
+ # topk_token_indices: [H, B, k], Req_to_tokens: [B, S]
761
+ # topk_token_indices = torch.arange(0, heavy_token_num, device=q.device).unsqueeze(0).unsqueeze(0).expand(q.shape[1], q.shape[0], -1)
762
+
763
+ sparse_flash_decode_stage2(
764
+ q,
765
+ k_buffer,
766
+ v_buffer,
767
+ req_to_token,
768
+ topk_token_indices,
769
+ heavy_token_num,
770
+ mid_out,
771
+ mid_o_logexpsum,
772
+ BLOCK_SEQ,
773
+ sm_scale,
774
+ )
775
+
776
+ sparse_flash_decode_stage3(heavy_token_num, mid_out, mid_o_logexpsum, o, BLOCK_SEQ)
777
+
778
+
779
+ # Extend attention kernel for Double Sparsity
780
+ # Moved from https://github.com/sgl-project/sglang/blob/v0.4.2.post1/python/sglang/srt/layers/attention/triton_ops/extend_attention.py
781
+ @triton.jit
782
+ def _fwd_kernel(
783
+ Q_Extend,
784
+ K_Extend,
785
+ V_Extend,
786
+ O_Extend,
787
+ K_Buffer,
788
+ V_Buffer,
789
+ Req_to_tokens,
790
+ B_req_idx,
791
+ B_Seq_Len,
792
+ B_Start_Loc_Extend,
793
+ B_Seq_Len_Extend,
794
+ sm_scale,
795
+ kv_group_num,
796
+ stride_qbs,
797
+ stride_qh,
798
+ stride_kbs,
799
+ stride_kh,
800
+ stride_vbs,
801
+ stride_vh,
802
+ stride_obs,
803
+ stride_oh,
804
+ stride_buf_kbs,
805
+ stride_buf_kh,
806
+ stride_buf_vbs,
807
+ stride_buf_vh,
808
+ stride_req_to_tokens_b,
809
+ logit_cap: tl.constexpr,
810
+ Lq: tl.constexpr,
811
+ Lv: tl.constexpr,
812
+ BLOCK_DMODEL: tl.constexpr,
813
+ BLOCK_DPE: tl.constexpr,
814
+ BLOCK_DV: tl.constexpr,
815
+ BLOCK_M: tl.constexpr,
816
+ BLOCK_N: tl.constexpr,
817
+ ):
818
+ cur_seq = tl.program_id(0)
819
+ cur_head = tl.program_id(1)
820
+ cur_block_m = tl.program_id(2)
821
+ cur_kv_head = cur_head // kv_group_num
822
+
823
+ cur_seq_len = tl.load(B_Seq_Len + cur_seq)
824
+ cur_seq_len_extend = tl.load(B_Seq_Len_Extend + cur_seq)
825
+ cur_seq_len_prefix = cur_seq_len - cur_seq_len_extend
826
+
827
+ cur_seq_prefix_start_in_loc = 0
828
+ cur_seq_extend_start_contiguous = tl.load(B_Start_Loc_Extend + cur_seq)
829
+ cur_batch_req_idx = tl.load(B_req_idx + cur_seq)
830
+
831
+ offs_d = tl.arange(0, BLOCK_DMODEL)
832
+ offs_dv = tl.arange(0, BLOCK_DV)
833
+ offs_m = tl.arange(0, BLOCK_M)
834
+ mask_m = (cur_block_m * BLOCK_M + offs_m) < cur_seq_len_extend
835
+
836
+ mask_d = offs_d < Lq
837
+ mask_dv = offs_dv < Lv
838
+
839
+ offs_q = (
840
+ (cur_seq_extend_start_contiguous + cur_block_m * BLOCK_M + offs_m[:, None])
841
+ * stride_qbs
842
+ + cur_head * stride_qh
843
+ + offs_d[None, :]
844
+ )
845
+ q = tl.load(
846
+ Q_Extend + offs_q, mask=(mask_m[:, None]) & (mask_d[None, :]), other=0.0
847
+ )
848
+
849
+ if BLOCK_DPE > 0:
850
+ offs_dpe = BLOCK_DMODEL + tl.arange(0, BLOCK_DPE)
851
+ offs_qpe = (
852
+ (cur_seq_extend_start_contiguous + cur_block_m * BLOCK_M + offs_m[:, None])
853
+ * stride_qbs
854
+ + cur_head * stride_qh
855
+ + offs_dpe[None, :]
856
+ )
857
+ qpe = tl.load(Q_Extend + offs_qpe, mask=mask_m[:, None], other=0.0)
858
+
859
+ # stage 1: compute scores with prefix
860
+ offs_n = tl.arange(0, BLOCK_N)
861
+
862
+ acc = tl.zeros([BLOCK_M, BLOCK_DV], dtype=tl.float32)
863
+ deno = tl.zeros([BLOCK_M], dtype=tl.float32)
864
+ e_max = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
865
+
866
+ for start_n in range(0, cur_seq_len_prefix, BLOCK_N):
867
+ start_n = tl.multiple_of(start_n, BLOCK_N)
868
+ mask_n = (start_n + offs_n) < cur_seq_len_prefix
869
+ offs_b_loc_prefix = cur_batch_req_idx * stride_req_to_tokens_b + (
870
+ cur_seq_prefix_start_in_loc + start_n + offs_n
871
+ )
872
+ offs_kv_loc = tl.load(Req_to_tokens + offs_b_loc_prefix, mask=mask_n, other=0)
873
+
874
+ # load k in transposed way
875
+ offs_buf_k = (
876
+ offs_kv_loc[None, :] * stride_buf_kbs
877
+ + cur_kv_head * stride_buf_kh
878
+ + offs_d[:, None]
879
+ )
880
+ k = tl.load(
881
+ K_Buffer + offs_buf_k, mask=(mask_n[None, :]) & (mask_d[:, None]), other=0.0
882
+ )
883
+
884
+ qk = tl.dot(q.to(k.dtype), k)
885
+ if BLOCK_DPE > 0:
886
+ offs_kpe = (
887
+ offs_kv_loc[None, :] * stride_buf_kbs
888
+ + cur_kv_head * stride_buf_kh
889
+ + offs_dpe[:, None]
890
+ )
891
+ kpe = tl.load(
892
+ K_Buffer + offs_kpe,
893
+ mask=mask_n[None, :],
894
+ other=0.0,
895
+ )
896
+ qk += tl.dot(qpe.to(kpe.dtype), kpe)
897
+ qk *= sm_scale
898
+
899
+ if logit_cap > 0:
900
+ qk = logit_cap * tanh(qk / logit_cap)
901
+
902
+ qk = tl.where(mask_m[:, None] & mask_n[None, :], qk, float("-inf"))
903
+
904
+ n_e_max = tl.maximum(tl.max(qk, 1), e_max)
905
+ re_scale = tl.exp(e_max - n_e_max)
906
+ p = tl.exp(qk - n_e_max[:, None])
907
+ deno = deno * re_scale + tl.sum(p, 1)
908
+
909
+ offs_buf_v = (
910
+ offs_kv_loc[:, None] * stride_buf_vbs
911
+ + cur_kv_head * stride_buf_vh
912
+ + offs_dv[None, :]
913
+ )
914
+ v = tl.load(
915
+ V_Buffer + offs_buf_v, mask=mask_n[:, None] & mask_dv[None, :], other=0.0
916
+ )
917
+ p = p.to(v.dtype)
918
+ acc = acc * re_scale[:, None] + tl.dot(p, v)
919
+
920
+ e_max = n_e_max
921
+
922
+ # stage 2: compute the trianlge part
923
+
924
+ cur_block_m_end = tl.minimum(cur_seq_len_extend, (cur_block_m + 1) * BLOCK_M)
925
+ for start_n in range(0, cur_block_m_end, BLOCK_N):
926
+ start_n = tl.multiple_of(start_n, BLOCK_N)
927
+ mask_n = (start_n + offs_n) < cur_block_m_end
928
+
929
+ # load k in transposed way
930
+ offs_k = (
931
+ (cur_seq_extend_start_contiguous + start_n + offs_n[None, :]) * stride_kbs
932
+ + cur_kv_head * stride_kh
933
+ + offs_d[:, None]
934
+ )
935
+ k = tl.load(
936
+ K_Extend + offs_k, mask=(mask_n[None, :]) & (mask_d[:, None]), other=0.0
937
+ )
938
+
939
+ qk = tl.dot(q, k, out_dtype=tl.float32)
940
+ if BLOCK_DPE > 0:
941
+ offs_kpe = (
942
+ (cur_seq_extend_start_contiguous + start_n + offs_n[None, :])
943
+ * stride_kbs
944
+ + cur_kv_head * stride_kh
945
+ + offs_dpe[:, None]
946
+ )
947
+ kpe = tl.load(
948
+ K_Extend + offs_kpe,
949
+ mask=mask_n[None, :],
950
+ other=0.0,
951
+ )
952
+ qk += tl.dot(qpe, kpe)
953
+
954
+ qk *= sm_scale
955
+
956
+ if logit_cap > 0:
957
+ qk = logit_cap * tanh(qk / logit_cap)
958
+
959
+ mask_causual = (cur_block_m * BLOCK_M + offs_m[:, None]) >= (
960
+ start_n + offs_n[None, :]
961
+ )
962
+ mask_causual &= mask_m[:, None] & mask_n[None, :]
963
+ qk = tl.where(mask_causual, qk, float("-inf"))
964
+
965
+ n_e_max = tl.maximum(tl.max(qk, 1), e_max)
966
+ re_scale = tl.exp(e_max - n_e_max)
967
+ p = tl.exp(qk - n_e_max[:, None])
968
+ deno = deno * re_scale + tl.sum(p, 1)
969
+
970
+ offs_v = (
971
+ (cur_seq_extend_start_contiguous + start_n + offs_n[:, None]) * stride_vbs
972
+ + cur_kv_head * stride_vh
973
+ + offs_dv[None, :]
974
+ )
975
+ v = tl.load(
976
+ V_Extend + offs_v, mask=mask_n[:, None] & mask_dv[None, :], other=0.0
977
+ )
978
+ p = p.to(v.dtype)
979
+ acc = acc * re_scale[:, None] + tl.dot(p, v)
980
+
981
+ e_max = n_e_max
982
+
983
+ offs_o = (
984
+ (cur_seq_extend_start_contiguous + cur_block_m * BLOCK_M + offs_m[:, None])
985
+ * stride_obs
986
+ + cur_head * stride_oh
987
+ + offs_dv[None, :]
988
+ )
989
+ tl.store(
990
+ O_Extend + offs_o, acc / deno[:, None], mask=mask_m[:, None] & mask_dv[None, :]
991
+ )
992
+
993
+
994
+ def extend_attention_fwd(
995
+ q_extend,
996
+ k_extend,
997
+ v_extend,
998
+ o_extend,
999
+ k_buffer,
1000
+ v_buffer,
1001
+ req_to_tokens,
1002
+ b_req_idx,
1003
+ b_seq_len,
1004
+ b_seq_len_extend,
1005
+ b_start_loc_extend,
1006
+ max_len_extend,
1007
+ sm_scale=None,
1008
+ logit_cap=0.0,
1009
+ ):
1010
+ """
1011
+ q_extend, k_extend, v_extend, o_extend: contiguous tensors
1012
+
1013
+ k_buffer, v_buffer: (prefix + extend) tensors in mem_manager
1014
+ """
1015
+ Lq, Lk, Lv = (
1016
+ q_extend.shape[-1],
1017
+ k_extend.shape[-1],
1018
+ v_extend.shape[-1],
1019
+ )
1020
+
1021
+ if Lq == 576:
1022
+ BLOCK_DMODEL = 512
1023
+ BLOCK_DPE = 64
1024
+ elif Lq == 288:
1025
+ BLOCK_DMODEL = 256
1026
+ BLOCK_DPE = 32
1027
+ elif Lq == 192:
1028
+ BLOCK_DMODEL = 128
1029
+ BLOCK_DPE = 64
1030
+ else:
1031
+ BLOCK_DMODEL = triton.next_power_of_2(Lq)
1032
+ BLOCK_DPE = 0
1033
+ BLOCK_DV = triton.next_power_of_2(Lv)
1034
+
1035
+ if _is_hip:
1036
+ BLOCK_M, BLOCK_N = (64, 64)
1037
+ num_warps = 4
1038
+
1039
+ else:
1040
+ if _is_cuda and CUDA_CAPABILITY[0] >= 9:
1041
+ if Lq <= 256:
1042
+ BLOCK_M, BLOCK_N = (128, 64)
1043
+ else:
1044
+ BLOCK_M, BLOCK_N = (32, 64)
1045
+ elif _is_cuda and CUDA_CAPABILITY[0] >= 8:
1046
+ if Lq <= 128:
1047
+ BLOCK_M, BLOCK_N = (128, 128)
1048
+ elif Lq <= 256:
1049
+ BLOCK_M, BLOCK_N = (64, 64)
1050
+ else:
1051
+ BLOCK_M, BLOCK_N = (32, 64)
1052
+ else:
1053
+ BLOCK_M, BLOCK_N = (64, 64) if Lq <= 128 else (32, 32)
1054
+
1055
+ num_warps = 4 if Lk <= 64 else 8
1056
+
1057
+ sm_scale = sm_scale or 1.0 / (Lq**0.5)
1058
+ batch_size, head_num = b_seq_len.shape[0], q_extend.shape[1]
1059
+ kv_group_num = q_extend.shape[1] // k_extend.shape[1]
1060
+
1061
+ grid = (batch_size, head_num, triton.cdiv(max_len_extend, BLOCK_M))
1062
+ num_stages = 1
1063
+
1064
+ extra_kargs = {}
1065
+ if _is_hip:
1066
+ extra_kargs = {"waves_per_eu": 4, "matrix_instr_nonkdim": 16, "kpack": 2}
1067
+
1068
+ _fwd_kernel[grid](
1069
+ q_extend,
1070
+ k_extend,
1071
+ v_extend,
1072
+ o_extend,
1073
+ k_buffer,
1074
+ v_buffer,
1075
+ req_to_tokens,
1076
+ b_req_idx,
1077
+ b_seq_len,
1078
+ b_start_loc_extend,
1079
+ b_seq_len_extend,
1080
+ sm_scale,
1081
+ kv_group_num,
1082
+ q_extend.stride(0),
1083
+ q_extend.stride(1),
1084
+ k_extend.stride(0),
1085
+ k_extend.stride(1),
1086
+ v_extend.stride(0),
1087
+ v_extend.stride(1),
1088
+ o_extend.stride(0),
1089
+ o_extend.stride(1),
1090
+ k_buffer.stride(0),
1091
+ k_buffer.stride(1),
1092
+ v_buffer.stride(0),
1093
+ v_buffer.stride(1),
1094
+ req_to_tokens.stride(0),
1095
+ logit_cap=logit_cap,
1096
+ BLOCK_DMODEL=BLOCK_DMODEL,
1097
+ BLOCK_DPE=BLOCK_DPE,
1098
+ BLOCK_DV=BLOCK_DV,
1099
+ BLOCK_M=BLOCK_M,
1100
+ BLOCK_N=BLOCK_N,
1101
+ Lq=Lq,
1102
+ Lv=Lv,
1103
+ num_warps=num_warps,
1104
+ num_stages=num_stages,
1105
+ **extra_kargs,
1106
+ )
lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/extend_attention.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023-2024 SGLang Team
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ==============================================================================
14
+ """
15
+ Memory-efficient attention for prefill.
16
+ It supports page size = 1 and prefill with KV cache (i.e. extend).
17
+ """
18
+
19
+ import torch
20
+ import triton
21
+ import triton.language as tl
22
+
23
+ from sglang.srt.layers.attention.triton_ops.prefill_attention import (
24
+ context_attention_fwd,
25
+ )
26
+ from sglang.srt.utils import is_cuda, is_hip
27
+
28
+ _is_cuda = is_cuda()
29
+ if _is_cuda:
30
+ CUDA_CAPABILITY = torch.cuda.get_device_capability()
31
+
32
+ _is_hip = is_hip()
33
+
34
+
35
+ @triton.jit
36
+ def tanh(x):
37
+ # Tanh is just a scaled sigmoid
38
+ return 2 * tl.sigmoid(2 * x) - 1
39
+
40
+
41
+ @triton.jit
42
+ def _fwd_kernel(
43
+ Q_Extend,
44
+ K_Extend,
45
+ V_Extend,
46
+ O_Extend,
47
+ K_Buffer,
48
+ V_Buffer,
49
+ qo_indptr,
50
+ kv_indptr,
51
+ kv_indices,
52
+ mask_ptr,
53
+ mask_indptr,
54
+ sm_scale,
55
+ kv_group_num,
56
+ stride_qbs,
57
+ stride_qh,
58
+ stride_kbs,
59
+ stride_kh,
60
+ stride_vbs,
61
+ stride_vh,
62
+ stride_obs,
63
+ stride_oh,
64
+ stride_buf_kbs,
65
+ stride_buf_kh,
66
+ stride_buf_vbs,
67
+ stride_buf_vh,
68
+ logit_cap: tl.constexpr,
69
+ Lq: tl.constexpr,
70
+ Lv: tl.constexpr,
71
+ BLOCK_DMODEL: tl.constexpr,
72
+ BLOCK_DPE: tl.constexpr,
73
+ BLOCK_DV: tl.constexpr,
74
+ BLOCK_M: tl.constexpr,
75
+ BLOCK_N: tl.constexpr,
76
+ USE_CUSTOM_MASK: tl.constexpr,
77
+ IS_CAUSAL: tl.constexpr,
78
+ SKIP_PREFIX_CUSTOM_MASK: tl.constexpr,
79
+ STORE_TRANSPOSE: tl.constexpr,
80
+ ):
81
+ cur_seq = tl.program_id(0)
82
+ cur_head = tl.program_id(1)
83
+ cur_block_m = tl.program_id(2)
84
+ cur_kv_head = cur_head // kv_group_num
85
+
86
+ cur_seq_extend_start_idx = tl.load(qo_indptr + cur_seq)
87
+ cur_seq_len_extend = tl.load(qo_indptr + cur_seq + 1) - cur_seq_extend_start_idx
88
+ cur_seq_kv_start_idx = tl.load(kv_indptr + cur_seq)
89
+ cur_seq_len_prefix = tl.load(kv_indptr + cur_seq + 1) - cur_seq_kv_start_idx
90
+ cur_seq_len = cur_seq_len_prefix + cur_seq_len_extend
91
+
92
+ if USE_CUSTOM_MASK:
93
+ cur_seq_mask_start_idx = tl.load(mask_indptr + cur_seq)
94
+
95
+ offs_d = tl.arange(0, BLOCK_DMODEL)
96
+ offs_dv = tl.arange(0, BLOCK_DV)
97
+ offs_m = tl.arange(0, BLOCK_M)
98
+ mask_m = (cur_block_m * BLOCK_M + offs_m) < cur_seq_len_extend
99
+
100
+ mask_d = offs_d < Lq
101
+ mask_dv = offs_dv < Lv
102
+
103
+ offs_q = (
104
+ (cur_seq_extend_start_idx + cur_block_m * BLOCK_M + offs_m[:, None])
105
+ * stride_qbs
106
+ + cur_head * stride_qh
107
+ + offs_d[None, :]
108
+ )
109
+ q = tl.load(
110
+ Q_Extend + offs_q, mask=(mask_m[:, None]) & (mask_d[None, :]), other=0.0
111
+ )
112
+
113
+ if BLOCK_DPE > 0:
114
+ offs_dpe = BLOCK_DMODEL + tl.arange(0, BLOCK_DPE)
115
+ offs_qpe = (
116
+ (cur_seq_extend_start_idx + cur_block_m * BLOCK_M + offs_m[:, None])
117
+ * stride_qbs
118
+ + cur_head * stride_qh
119
+ + offs_dpe[None, :]
120
+ )
121
+ qpe = tl.load(Q_Extend + offs_qpe, mask=mask_m[:, None], other=0.0)
122
+
123
+ # stage 1: compute scores with prefix
124
+ offs_n = tl.arange(0, BLOCK_N)
125
+
126
+ acc = tl.zeros([BLOCK_M, BLOCK_DV], dtype=tl.float32)
127
+ deno = tl.zeros([BLOCK_M], dtype=tl.float32)
128
+ e_max = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
129
+
130
+ for start_n in range(0, cur_seq_len_prefix, BLOCK_N):
131
+ start_n = tl.multiple_of(start_n, BLOCK_N)
132
+ mask_n = (start_n + offs_n) < cur_seq_len_prefix
133
+
134
+ offs_kv_loc = tl.load(
135
+ kv_indices + cur_seq_kv_start_idx + start_n + offs_n, mask=mask_n, other=0
136
+ )
137
+
138
+ # load k in transposed way
139
+ offs_buf_k = (
140
+ offs_kv_loc[None, :] * stride_buf_kbs
141
+ + cur_kv_head * stride_buf_kh
142
+ + offs_d[:, None]
143
+ )
144
+ k = tl.load(
145
+ K_Buffer + offs_buf_k, mask=(mask_n[None, :]) & (mask_d[:, None]), other=0.0
146
+ )
147
+
148
+ qk = tl.dot(q.to(k.dtype), k)
149
+ if BLOCK_DPE > 0:
150
+ offs_kpe = (
151
+ offs_kv_loc[None, :] * stride_buf_kbs
152
+ + cur_kv_head * stride_buf_kh
153
+ + offs_dpe[:, None]
154
+ )
155
+ kpe = tl.load(
156
+ K_Buffer + offs_kpe,
157
+ mask=mask_n[None, :],
158
+ other=0.0,
159
+ )
160
+ qk += tl.dot(qpe.to(kpe.dtype), kpe)
161
+ qk *= sm_scale
162
+
163
+ if logit_cap > 0:
164
+ qk = logit_cap * tanh(qk / logit_cap)
165
+
166
+ if USE_CUSTOM_MASK and not SKIP_PREFIX_CUSTOM_MASK:
167
+ custom_mask = tl.load(
168
+ mask_ptr
169
+ + cur_seq_mask_start_idx
170
+ + (cur_block_m * BLOCK_M + offs_m[:, None]) * cur_seq_len
171
+ + start_n
172
+ + offs_n[None, :],
173
+ mask=(mask_m[:, None] & mask_n[None, :]),
174
+ other=0,
175
+ )
176
+ custom_mask &= mask_m[:, None] & mask_n[None, :]
177
+ qk = tl.where(custom_mask, qk, float("-inf"))
178
+ else:
179
+ qk = tl.where(mask_m[:, None] & mask_n[None, :], qk, float("-inf"))
180
+
181
+ n_e_max = tl.maximum(tl.max(qk, 1), e_max)
182
+ re_scale = tl.exp(e_max - n_e_max)
183
+ p = tl.exp(qk - n_e_max[:, None])
184
+ deno = deno * re_scale + tl.sum(p, 1)
185
+
186
+ offs_buf_v = (
187
+ offs_kv_loc[:, None] * stride_buf_vbs
188
+ + cur_kv_head * stride_buf_vh
189
+ + offs_dv[None, :]
190
+ )
191
+ v = tl.load(
192
+ V_Buffer + offs_buf_v, mask=mask_n[:, None] & mask_dv[None, :], other=0.0
193
+ )
194
+ p = p.to(v.dtype)
195
+ acc = acc * re_scale[:, None] + tl.dot(p, v)
196
+
197
+ e_max = n_e_max
198
+
199
+ # stage 2: compute the triangle part
200
+
201
+ cur_block_m_end = (
202
+ cur_seq_len_extend
203
+ if not IS_CAUSAL
204
+ else tl.minimum(cur_seq_len_extend, (cur_block_m + 1) * BLOCK_M)
205
+ )
206
+ for start_n in range(0, cur_block_m_end, BLOCK_N):
207
+ start_n = tl.multiple_of(start_n, BLOCK_N)
208
+ mask_n = (start_n + offs_n) < cur_block_m_end
209
+
210
+ # load k in transposed way
211
+ offs_k = (
212
+ (cur_seq_extend_start_idx + start_n + offs_n[None, :]) * stride_kbs
213
+ + cur_kv_head * stride_kh
214
+ + offs_d[:, None]
215
+ )
216
+ k = tl.load(
217
+ K_Extend + offs_k, mask=(mask_n[None, :]) & (mask_d[:, None]), other=0.0
218
+ )
219
+
220
+ qk = tl.dot(q, k, out_dtype=tl.float32)
221
+ if BLOCK_DPE > 0:
222
+ offs_kpe = (
223
+ (cur_seq_extend_start_idx + start_n + offs_n[None, :]) * stride_kbs
224
+ + cur_kv_head * stride_kh
225
+ + offs_dpe[:, None]
226
+ )
227
+ kpe = tl.load(
228
+ K_Extend + offs_kpe,
229
+ mask=mask_n[None, :],
230
+ other=0.0,
231
+ )
232
+ qk += tl.dot(qpe, kpe)
233
+
234
+ qk *= sm_scale
235
+
236
+ if logit_cap > 0:
237
+ qk = logit_cap * tanh(qk / logit_cap)
238
+
239
+ if USE_CUSTOM_MASK:
240
+ custom_mask = tl.load(
241
+ mask_ptr
242
+ + cur_seq_mask_start_idx
243
+ + (cur_block_m * BLOCK_M + offs_m[:, None]) * cur_seq_len
244
+ + cur_seq_len_prefix
245
+ + start_n
246
+ + offs_n[None, :],
247
+ mask=(mask_m[:, None] & mask_n[None, :]),
248
+ other=0,
249
+ )
250
+ custom_mask &= mask_m[:, None] & mask_n[None, :]
251
+ qk = tl.where(custom_mask, qk, float("-inf"))
252
+ elif IS_CAUSAL:
253
+ mask_causual = (cur_block_m * BLOCK_M + offs_m[:, None]) >= (
254
+ start_n + offs_n[None, :]
255
+ )
256
+ mask_causual &= mask_m[:, None] & mask_n[None, :]
257
+ qk = tl.where(mask_causual, qk, float("-inf"))
258
+ else:
259
+ mask_non_causal = mask_m[:, None] & mask_n[None, :]
260
+ qk = tl.where(mask_non_causal, qk, float("-inf"))
261
+
262
+ n_e_max = tl.maximum(tl.max(qk, 1), e_max)
263
+ re_scale = tl.exp(e_max - n_e_max)
264
+ p = tl.exp(qk - n_e_max[:, None])
265
+ deno = deno * re_scale + tl.sum(p, 1)
266
+
267
+ offs_v = (
268
+ (cur_seq_extend_start_idx + start_n + offs_n[:, None]) * stride_vbs
269
+ + cur_kv_head * stride_vh
270
+ + offs_dv[None, :]
271
+ )
272
+ v = tl.load(
273
+ V_Extend + offs_v, mask=mask_n[:, None] & mask_dv[None, :], other=0.0
274
+ )
275
+ p = p.to(v.dtype)
276
+ acc = acc * re_scale[:, None] + tl.dot(p, v)
277
+
278
+ e_max = n_e_max
279
+
280
+ offs_o = (
281
+ (cur_seq_extend_start_idx + cur_block_m * BLOCK_M + offs_m[:, None])
282
+ * stride_obs
283
+ + cur_head * stride_oh
284
+ + offs_dv[None, :]
285
+ )
286
+ if STORE_TRANSPOSE:
287
+ tl.store(
288
+ O_Extend + offs_o.T,
289
+ (acc / deno[:, None]).T,
290
+ mask=(mask_m[:, None] & mask_dv[None, :]).T,
291
+ )
292
+ else:
293
+ tl.store(
294
+ O_Extend + offs_o,
295
+ acc / deno[:, None],
296
+ mask=mask_m[:, None] & mask_dv[None, :],
297
+ )
298
+
299
+
300
+ def extend_attention_fwd(
301
+ q_extend,
302
+ k_extend,
303
+ v_extend,
304
+ o_extend,
305
+ k_buffer,
306
+ v_buffer,
307
+ qo_indptr,
308
+ kv_indptr,
309
+ kv_indices,
310
+ custom_mask,
311
+ is_causal,
312
+ mask_indptr,
313
+ max_len_extend,
314
+ sm_scale=None,
315
+ logit_cap=0.0,
316
+ skip_prefix_custom_mask=True,
317
+ ):
318
+ """
319
+ q_extend, k_extend, v_extend, o_extend: contiguous tensors
320
+
321
+ k_buffer, v_buffer: (prefix + extend) tensors in mem_manager
322
+ """
323
+ Lq, Lk, Lv = (
324
+ q_extend.shape[-1],
325
+ k_extend.shape[-1],
326
+ v_extend.shape[-1],
327
+ )
328
+
329
+ if Lq == 576:
330
+ BLOCK_DMODEL = 512
331
+ BLOCK_DPE = 64
332
+ elif Lq == 288:
333
+ BLOCK_DMODEL = 256
334
+ BLOCK_DPE = 32
335
+ elif Lq == 192:
336
+ BLOCK_DMODEL = 128
337
+ BLOCK_DPE = 64
338
+ else:
339
+ BLOCK_DMODEL = triton.next_power_of_2(Lq)
340
+ BLOCK_DPE = 0
341
+ BLOCK_DV = triton.next_power_of_2(Lv)
342
+
343
+ if _is_hip:
344
+ BLOCK_M, BLOCK_N = (64, 64)
345
+ num_warps = 4
346
+
347
+ else:
348
+ if _is_cuda and CUDA_CAPABILITY[0] >= 9:
349
+ if Lq <= 256:
350
+ BLOCK_M, BLOCK_N = (128, 64)
351
+ else:
352
+ BLOCK_M, BLOCK_N = (32, 64)
353
+ elif _is_cuda and CUDA_CAPABILITY[0] >= 8:
354
+ # sm86/sm89 has a much smaller shared memory size (100K) than sm80 (160K)
355
+ if CUDA_CAPABILITY[1] == 9 or CUDA_CAPABILITY[1] == 6:
356
+ if Lq <= 128:
357
+ BLOCK_M, BLOCK_N = (64, 128)
358
+ elif Lq <= 256:
359
+ BLOCK_M, BLOCK_N = (64, 64)
360
+ else:
361
+ BLOCK_M, BLOCK_N = (32, 32)
362
+ else:
363
+ if Lq <= 128:
364
+ BLOCK_M, BLOCK_N = (128, 128)
365
+ elif Lq <= 256:
366
+ BLOCK_M, BLOCK_N = (64, 64)
367
+ else:
368
+ BLOCK_M, BLOCK_N = (32, 64)
369
+ else:
370
+ BLOCK_M, BLOCK_N = (64, 64) if Lq <= 128 else (32, 32)
371
+
372
+ num_warps = 4 if Lk <= 64 else 8
373
+
374
+ sm_scale = sm_scale or 1.0 / (Lq**0.5)
375
+ batch_size, head_num = qo_indptr.shape[0] - 1, q_extend.shape[1]
376
+ kv_group_num = q_extend.shape[1] // k_extend.shape[1]
377
+
378
+ USE_CUSTOM_MASK = custom_mask is not None
379
+ # Skip custom mask for prefix part
380
+ SKIP_PREFIX_CUSTOM_MASK = skip_prefix_custom_mask
381
+
382
+ grid = (batch_size, head_num, triton.cdiv(max_len_extend, BLOCK_M))
383
+ num_stages = 1
384
+
385
+ extra_kargs = {}
386
+ if _is_hip:
387
+ extra_kargs = {"waves_per_eu": 1, "matrix_instr_nonkdim": 16, "kpack": 2}
388
+
389
+ _fwd_kernel[grid](
390
+ q_extend,
391
+ k_extend,
392
+ v_extend,
393
+ o_extend,
394
+ k_buffer,
395
+ v_buffer,
396
+ qo_indptr,
397
+ kv_indptr,
398
+ kv_indices,
399
+ custom_mask,
400
+ mask_indptr,
401
+ sm_scale,
402
+ kv_group_num,
403
+ q_extend.stride(0),
404
+ q_extend.stride(1),
405
+ k_extend.stride(0),
406
+ k_extend.stride(1),
407
+ v_extend.stride(0),
408
+ v_extend.stride(1),
409
+ o_extend.stride(0),
410
+ o_extend.stride(1),
411
+ k_buffer.stride(0),
412
+ k_buffer.stride(1),
413
+ v_buffer.stride(0),
414
+ v_buffer.stride(1),
415
+ logit_cap=logit_cap,
416
+ BLOCK_DMODEL=BLOCK_DMODEL,
417
+ BLOCK_DPE=BLOCK_DPE,
418
+ BLOCK_DV=BLOCK_DV,
419
+ BLOCK_M=BLOCK_M,
420
+ BLOCK_N=BLOCK_N,
421
+ Lq=Lq,
422
+ Lv=Lv,
423
+ USE_CUSTOM_MASK=USE_CUSTOM_MASK,
424
+ IS_CAUSAL=is_causal,
425
+ SKIP_PREFIX_CUSTOM_MASK=SKIP_PREFIX_CUSTOM_MASK,
426
+ STORE_TRANSPOSE=_is_hip,
427
+ num_warps=num_warps,
428
+ num_stages=num_stages,
429
+ **extra_kargs,
430
+ )
431
+
432
+
433
+ def redundant_attention(
434
+ q_extend,
435
+ o_extend,
436
+ k_buffer,
437
+ v_buffer,
438
+ b_req_idx,
439
+ b_start_loc,
440
+ b_seq_len,
441
+ b_seq_len_prefix,
442
+ max_len_in_batch,
443
+ ):
444
+ total_token_num = k_buffer.shape[0]
445
+ B, H_Q, D = b_req_idx.shape[0], q_extend.shape[-2], q_extend.shape[-1]
446
+ q_buffer = torch.empty(
447
+ (total_token_num, H_Q, D), dtype=q_extend.dtype, device=q_extend.device
448
+ )
449
+
450
+ pt = 0
451
+ for i in range(B):
452
+ cur_seq_len_extend = b_seq_len[i] - b_seq_len_prefix[i]
453
+ pl, pr = b_start_loc[i] + b_seq_len_prefix[i], b_start_loc[i] + b_seq_len[i]
454
+ q_buffer[pl:pr] = q_extend[pt : pt + cur_seq_len_extend]
455
+ pt += cur_seq_len_extend
456
+
457
+ o_buffer = torch.empty_like(q_buffer)
458
+ context_attention_fwd(
459
+ q_buffer, k_buffer, v_buffer, o_buffer, b_start_loc, b_seq_len, max_len_in_batch
460
+ )
461
+
462
+ pt = 0
463
+ for i in range(B):
464
+ cur_seq_len_extend = b_seq_len[i] - b_seq_len_prefix[i]
465
+ pl, pr = b_start_loc[i] + b_seq_len_prefix[i], b_start_loc[i] + b_seq_len[i]
466
+ o_extend[pt : pt + cur_seq_len_extend] = o_buffer[pl:pr]
467
+ pt += cur_seq_len_extend