Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/matplotlib/_docstring.pyi +34 -0
- lib/python3.12/site-packages/matplotlib/_enums.py +177 -0
- lib/python3.12/site-packages/matplotlib/_type1font.py +879 -0
- lib/python3.12/site-packages/matplotlib/backend_managers.pyi +64 -0
- lib/python3.12/site-packages/matplotlib/category.py +235 -0
- lib/python3.12/site-packages/matplotlib/cbook.py +2415 -0
- lib/python3.12/site-packages/matplotlib/dviread.py +1139 -0
- lib/python3.12/site-packages/matplotlib/dviread.pyi +89 -0
- lib/python3.12/site-packages/matplotlib/inset.py +269 -0
- lib/python3.12/site-packages/matplotlib/layout_engine.py +309 -0
- lib/python3.12/site-packages/matplotlib/legend.py +1370 -0
- lib/python3.12/site-packages/matplotlib/lines.py +1720 -0
- lib/python3.12/site-packages/matplotlib/mathtext.py +140 -0
- lib/python3.12/site-packages/matplotlib/offsetbox.pyi +298 -0
- lib/python3.12/site-packages/matplotlib/pyplot.py +0 -0
- lib/python3.12/site-packages/matplotlib/sankey.pyi +61 -0
- lib/python3.12/site-packages/matplotlib/scale.py +767 -0
- lib/python3.12/site-packages/matplotlib/texmanager.py +368 -0
- lib/python3.12/site-packages/matplotlib/transforms.py +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/code_completion_parser.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/custom_op.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/model_parallel.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/patch_torch.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/reasoning_parser.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/__pycache__/utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/activation.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/dp_attention.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/elementwise.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/layernorm.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/linear.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/logits_processor.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/parameter.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/pooler.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/radix_attention.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/rotary_embedding.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/sampler.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/torchao_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/__pycache__/vocab_parallel_embedding.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/base_attn_backend.py +111 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/cutlass_mla_backend.py +278 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/double_sparsity_backend.py +257 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/flashattention_backend.py +1851 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_backend.py +1266 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/flashinfer_mla_backend.py +840 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/flashmla_backend.py +281 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/torch_native_backend.py +267 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/triton_backend.py +733 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/decode_attention.py +733 -0
- lib/python3.12/site-packages/sglang/srt/layers/attention/triton_ops/double_sparsity_attention.py +1106 -0
- 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
|