gqa(adr-0065): D8 — composite returns output handle + output-space DMA
tl.composite now returns the output TensorHandle (not a CompletionHandle) so its result chains like tl.dot's; the handle carries the completion in a CompositeFuture pending so downstream ops and tl.wait auto-await it. out is a handle: out=tl.ref(addr,shape) (HBM, DMA_WRITE inside the composite) or an in-place TCM handle (STORE only); omitted -> TLContext auto-allocates a TCM scratch. out_ptr kept as HBM shorthand (= out=tl.ref(out_ptr, shape)) to avoid churning ~30 existing call sites. tl.ref now returns space=hbm (it references HBM data; operand-input DMA stays pinned-based per D4 so input streaming is unchanged). tiling: the tile loop's DMA_WRITE is gated on out.space==hbm (out analog of the operand pinned rule) — a TCM output stays on-chip (chainable) and its high-bit scratch address no longer hits the DMA PA decoder. Fixes the opt2 data-mode crash: the recipe accumulator O is TCM -> no DMA_WRITE -> opt2 now RUNS end-to-end in data mode (enable_data=True). Numeric parity of the recipe MATH ops is the next step. Suite 812 pass / 3 pre-existing fail; existing composite benches use out_ptr->hbm->DMA_WRITE unchanged (byte-equal). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -241,6 +241,41 @@ TLContext (compiler analog) PE_SCHEDULER (scheduler/dispatcher)
|
||||
imports: recipe 관련 없음
|
||||
```
|
||||
|
||||
### D8. Composite 출력 handle + 출력-space DMA
|
||||
|
||||
`tl.composite(...)` 는 **출력 `TensorHandle`** 을 반환(단순 `CompletionHandle`
|
||||
아님) — composite 결과를 `tl.dot` 결과처럼 downstream op 에 먹일 수 있음.
|
||||
handle 의 `pending` 필드가 composite 완료를 참조; downstream consumer 가
|
||||
auto-await(`_await_pending`, ADR-0062 lazy 패턴), `tl.wait(handle)` 동작.
|
||||
|
||||
**`out` 은 항상 `TensorHandle`** (raw 주소 아님) — 위치는 커널의 선택, 추측 안 함:
|
||||
|
||||
- **HBM 출력** → `out=tl.ref(addr, shape)`. `tl.ref` 가 `space="hbm"` handle
|
||||
반환 → composite 가 DMA_WRITE 로 write-back. STORE + DMA_WRITE 는 composite
|
||||
파이프라인 **내부**에 유지 — fused op 의 일부이지 별도 `tl.store` 아님.
|
||||
- **In-place TCM** → `out=<기존 TCM handle>` (예: recipe 누적기 `O`).
|
||||
`space="tcm"` → STORE 만, 결과 TCM 에 남음 (chainable).
|
||||
- **미지정** → TLContext 가 **TCM scratch 자동 할당** (`tl.dot` 과 동일) 후 반환.
|
||||
|
||||
편의상 `out_ptr: int` 는 **HBM 출력 shorthand** 로 허용 —
|
||||
`out=tl.ref(out_ptr, <출력 shape>)` 와 동치(컴파일러가 `space="hbm"` handle 로
|
||||
감쌈). 출력 미지정 시 scratch 주소는 커널이 아니라 컴파일러가 소유.
|
||||
(`tl.ref` 는 `space="hbm"` 반환 — HBM 데이터 참조이므로. operand-**입력** DMA
|
||||
결정은 D4 대로 `pinned` 기반이라 이 라벨이 입력 스트리밍에 영향 없음.)
|
||||
|
||||
출력 handle 의 **`space` 가 타일 루프의 write-back 을 결정** (D4 의 operand
|
||||
`pinned` 규칙의 출력판):
|
||||
|
||||
| `out.space` | 타일 루프 write-back |
|
||||
| --- | --- |
|
||||
| `"tcm"` | STORE 만 — TCM 에 남음 (chainable; **DMA_WRITE 없음**) |
|
||||
| `"hbm"` | STORE + DMA_WRITE — 최종 HBM 출력 |
|
||||
|
||||
이로써 출력이 TCM 에 남는 composite(예: recipe 누적기)가 합법이 되고,
|
||||
TCM-scratch 주소가 DMA 엔진의 PA 디코더(고비트 scratch 주소를 거부)로 가는
|
||||
일을 막음. 기존 벤치는 HBM 출력을 `tl.ref` 로 감싸므로 write-back
|
||||
불변(byte-equal).
|
||||
|
||||
## Alternatives
|
||||
|
||||
### A1. tile 당 3 composite (prologue 개념 없이)
|
||||
|
||||
@@ -267,6 +267,46 @@ TLContext (compiler analog) PE_SCHEDULER (scheduler/dispatcher)
|
||||
imports: nothing recipe-related
|
||||
```
|
||||
|
||||
### D8. Composite output handle + output-space DMA
|
||||
|
||||
`tl.composite(...)` returns the **output `TensorHandle`** (not a bare
|
||||
`CompletionHandle`), so a composite's result can feed a downstream op the
|
||||
same way `tl.dot`'s result does. The handle's `pending` field references
|
||||
the composite's completion; downstream consumers auto-await it
|
||||
(`_await_pending`, ADR-0062 lazy pattern) and `tl.wait(handle)` works.
|
||||
|
||||
**`out` is always a `TensorHandle`** (never a raw address) — its location is
|
||||
the kernel's choice, never guessed:
|
||||
|
||||
- **HBM output** → `out=tl.ref(addr, shape)`. `tl.ref` returns a handle with
|
||||
`space="hbm"`, so the composite writes it back via DMA_WRITE. The
|
||||
STORE + DMA_WRITE stay **inside** the composite pipeline — they are part of
|
||||
the fused op, *not* a separate `tl.store`.
|
||||
- **In-place TCM** → `out=<existing TCM handle>` (e.g. the recipe accumulator
|
||||
`O`). `space="tcm"` → STORE only, result stays in TCM (chainable).
|
||||
- **Omitted** → TLContext **auto-allocates a TCM scratch** (like `tl.dot`)
|
||||
and returns it.
|
||||
|
||||
For ergonomics, `out_ptr: int` is accepted as **shorthand for an HBM
|
||||
output** — equivalent to `out=tl.ref(out_ptr, <output shape>)` (the compiler
|
||||
wraps it into an `space="hbm"` handle). When no output is specified the
|
||||
compiler owns the scratch address, never the kernel. (`tl.ref` returns
|
||||
`space="hbm"` — it references HBM data. The operand-**input** DMA decision
|
||||
stays `pinned`-based per D4, so this label does not affect input streaming.)
|
||||
|
||||
The output handle's **`space` drives the tile loop's write-back** (the
|
||||
output analog of the operand `pinned` rule in D4):
|
||||
|
||||
| `out.space` | tile-loop write-back |
|
||||
| --- | --- |
|
||||
| `"tcm"` | STORE only — result stays in TCM (chainable; **no DMA_WRITE**) |
|
||||
| `"hbm"` | STORE + DMA_WRITE — final HBM output |
|
||||
|
||||
This makes a TCM-resident composite output (e.g. a recipe accumulator) legal
|
||||
**and** keeps a TCM-scratch address out of the DMA engine's PA decoder
|
||||
(which would otherwise reject the high-bit scratch address). Existing benches
|
||||
wrap their HBM output via `tl.ref` → write-back unchanged (byte-equal).
|
||||
|
||||
## Alternatives
|
||||
|
||||
### A1. Three composites per tile (no prologue concept)
|
||||
|
||||
@@ -67,9 +67,9 @@ def gqa_attention_decode_opt2_kernel(
|
||||
shape=(d_head, rest), dtype="f16")
|
||||
V1 = tl.ref(v_ptr + half * KV_ROW_BYTES, shape=(rest, d_head),
|
||||
dtype="f16")
|
||||
# #1: Q·Kᵀ composite → score tile (TCM-resident, consumed by #2).
|
||||
scores1 = tl.zeros((G * T_q, rest), dtype="f16")
|
||||
tl.composite(op="gemm", a=Q, b=K_T1, out=scores1)
|
||||
# #1: Q·Kᵀ composite → score tile. No `out` → auto-allocated TCM
|
||||
# scratch returned as a handle (ADR-0065 D8), consumed by #2.
|
||||
scores1 = tl.composite(op="gemm", a=Q, b=K_T1)
|
||||
# #2: softmax_merge recipe (online merge of (m,l,O)) + P·V + add.
|
||||
tl.composite(
|
||||
prologue=[{"op": "softmax_merge", "s": scores1,
|
||||
|
||||
@@ -24,6 +24,7 @@ def generate_gemm_plan(
|
||||
a_pinned: bool = False,
|
||||
b_pinned: bool = False,
|
||||
epilogue_specs: tuple = (),
|
||||
out_space: str = "hbm",
|
||||
) -> PipelinePlan:
|
||||
"""Generate GEMM tile plan: M→N→K order.
|
||||
|
||||
@@ -152,6 +153,11 @@ def generate_gemm_plan(
|
||||
"nbytes": out_bytes,
|
||||
},
|
||||
))
|
||||
# DMA_WRITE only when the output is HBM-resident
|
||||
# (ADR-0065 D8). A TCM output stays on-chip (chainable);
|
||||
# writing its high-bit scratch address as an HBM PA would
|
||||
# also fail the DMA decoder.
|
||||
if out_space == "hbm":
|
||||
stages.append(Stage(
|
||||
stage_type=StageType.DMA_WRITE,
|
||||
component=dma_id,
|
||||
@@ -281,6 +287,7 @@ def generate_plan_from_ops(
|
||||
a_pinned=getattr(a, "pinned", False),
|
||||
b_pinned=getattr(b, "pinned", False),
|
||||
epilogue_specs=tuple(post_ops),
|
||||
out_space=getattr(head.out, "space", "hbm"),
|
||||
)
|
||||
|
||||
# Prologue (pre-GEMM KERNEL) + post-loop KERNEL ops become single-shot
|
||||
|
||||
@@ -64,6 +64,23 @@ class LoadFuture:
|
||||
self.data: object = None
|
||||
|
||||
|
||||
class CompositeFuture:
|
||||
"""Pending marker on a composite's output handle (ADR-0065 D8).
|
||||
|
||||
Carries the composite's ``CompletionHandle`` so a downstream op (via
|
||||
``_await_pending``) or ``tl.wait`` can await it. Distinct from
|
||||
``LoadFuture``: composite completion is tracked by completion id in the
|
||||
runner's pending dict, so awaiting just re-issues a ``WaitCmd`` (a
|
||||
second wait on an already-drained completion is a harmless no-op).
|
||||
"""
|
||||
|
||||
__slots__ = ("completion", "resolved")
|
||||
|
||||
def __init__(self, completion: "CompletionHandle") -> None:
|
||||
self.completion = completion
|
||||
self.resolved: bool = False
|
||||
|
||||
|
||||
class _ScratchScope:
|
||||
"""Context manager that recycles per-tile scratch (ADR-0063 D1).
|
||||
|
||||
@@ -256,9 +273,14 @@ class TLContext:
|
||||
"""Create a TensorHandle referencing HBM data without issuing DMA.
|
||||
|
||||
Used when the scheduler will stream data per-tile (e.g., tensor b
|
||||
in a composite GEMM). No command is generated.
|
||||
in a composite GEMM) or as a composite's HBM output handle
|
||||
(ADR-0065 D8). The handle's ``space="hbm"`` — it references HBM
|
||||
data. No command is generated. (Operand-input DMA stays
|
||||
``pinned``-based per ADR-0065 D4, so this label does not change
|
||||
input streaming.)
|
||||
"""
|
||||
return self._make_handle(addr=ptr, shape=shape, dtype=dtype)
|
||||
return self._make_handle(addr=ptr, shape=shape, dtype=dtype,
|
||||
space="hbm")
|
||||
|
||||
# ── Data Movement (blocking, DMA engine) ──────────────────────
|
||||
|
||||
@@ -333,6 +355,11 @@ class TLContext:
|
||||
pending = getattr(h, "pending", None)
|
||||
if pending is None or pending.resolved:
|
||||
continue
|
||||
if isinstance(pending, CompositeFuture):
|
||||
# Composite output: wait on its completion (ADR-0065 D8).
|
||||
self._emit(WaitCmd(handle=pending.completion))
|
||||
pending.resolved = True
|
||||
else:
|
||||
self._runner.switch_to_simpy(("load_await", h))
|
||||
|
||||
def store(self, ptr: int, handle: TensorHandle) -> None:
|
||||
@@ -737,26 +764,33 @@ class TLContext:
|
||||
op: Literal["gemm", "math"],
|
||||
a: TensorHandle | None = None,
|
||||
b: TensorHandle | None = None,
|
||||
out_ptr: int = 0,
|
||||
math_op: str | None = None,
|
||||
*,
|
||||
out: TensorHandle | None = None,
|
||||
out_ptr: int | None = None,
|
||||
prologue: list[dict] | None = None,
|
||||
epilogue: list[dict] | None = None,
|
||||
acc_dtype: str | None = None,
|
||||
tile_shape: tuple[int, int, int] | None = None,
|
||||
) -> CompletionHandle:
|
||||
) -> TensorHandle:
|
||||
"""Submit a composite command (non-blocking, tiled pipeline).
|
||||
|
||||
``prologue`` (ADR-0065 D5) is an ordered list of recipe dicts — each
|
||||
with an ``"op"`` naming a ``RECIPE_DESCRIPTORS`` entry plus its
|
||||
operands. TLContext expands each into flat MATH OpSpecs that run
|
||||
before the head op and auto-binds the recipe's primary output into
|
||||
the head GEMM's ``a`` operand (D6.6). ``out`` (TensorHandle) is the
|
||||
explicit write-back handle, preferred over ``out_ptr``. ``epilogue``
|
||||
is an ordered list of ``EPILOGUE_OPS`` dicts.
|
||||
the head GEMM's ``a`` operand (D6.6). ``epilogue`` is an ordered list
|
||||
of ``EPILOGUE_OPS`` dicts.
|
||||
|
||||
Returns CompletionHandle for use with wait().
|
||||
``out`` (ADR-0065 D8) is a ``TensorHandle``: an HBM ref
|
||||
(``tl.ref(addr, shape)`` → ``space="hbm"`` → DMA_WRITE) or an
|
||||
in-place TCM handle (e.g. a recipe accumulator → STORE only).
|
||||
``out_ptr`` (int) is shorthand for an HBM output — equivalent to
|
||||
``out=tl.ref(out_ptr, <output shape>)``. If neither is given, a TCM
|
||||
scratch is auto-allocated (chainable, like ``tl.dot``).
|
||||
|
||||
Returns the **output TensorHandle** — auto-awaited by downstream ops
|
||||
(and by ``tl.wait(handle)``) via its ``pending`` field.
|
||||
"""
|
||||
# ADR-0062: composite operand DMA paths still need their inputs
|
||||
# to be resolved before the composite reads them via PE_SCHEDULER.
|
||||
@@ -798,14 +832,25 @@ class TLContext:
|
||||
out_shape = a.shape
|
||||
out_nbytes = a.nbytes
|
||||
|
||||
# Write-back handle: explicit `out` handle wins over `out_ptr`.
|
||||
# Write-back handle (ADR-0065 D8). Explicit `out` (HBM ref or an
|
||||
# in-place TCM accumulator) wins; `out_ptr` is HBM shorthand; else
|
||||
# auto-allocate a TCM scratch — the chainable default, like tl.dot.
|
||||
# pinned=True on the auto output: it is TCM-resident, so a downstream
|
||||
# op reading it needs no DMA_READ.
|
||||
if out is not None:
|
||||
out_handle = out
|
||||
else:
|
||||
elif out_ptr is not None:
|
||||
out_handle = TensorHandle(
|
||||
id=self._next_handle_id(),
|
||||
addr=out_ptr, shape=out_shape, dtype=out_dtype,
|
||||
nbytes=out_nbytes, space="tcm",
|
||||
nbytes=out_nbytes, space="hbm",
|
||||
)
|
||||
else:
|
||||
out_handle = TensorHandle(
|
||||
id=self._next_handle_id(),
|
||||
addr=self._scratch_alloc(out_nbytes),
|
||||
shape=out_shape, dtype=out_dtype, nbytes=out_nbytes,
|
||||
space="tcm", pinned=True,
|
||||
)
|
||||
|
||||
# Head op (flat-ops, ADR-0065 D2): GEMM takes named a/b and carries
|
||||
@@ -836,7 +881,12 @@ class TLContext:
|
||||
self._emit(CompositeCmd(
|
||||
completion=completion, ops=ops_tuple, rw_handles=rw_handles,
|
||||
))
|
||||
return completion
|
||||
# Attach the completion to the output handle so downstream ops (and
|
||||
# tl.wait) auto-await it (ADR-0065 D8 / ADR-0062 lazy pattern), and
|
||||
# return it so the result chains like tl.dot's (frozen dataclass →
|
||||
# set pending via object.__setattr__).
|
||||
object.__setattr__(out_handle, "pending", CompositeFuture(completion))
|
||||
return out_handle
|
||||
|
||||
def _expand_prologue(
|
||||
self, prologue: list[dict],
|
||||
@@ -1041,7 +1091,16 @@ class TLContext:
|
||||
handle.result = th
|
||||
return th
|
||||
|
||||
# Composite path (existing behaviour)
|
||||
# Composite output handle (ADR-0065 D8): wait on its completion.
|
||||
if isinstance(handle, TensorHandle):
|
||||
pending = getattr(handle, "pending", None)
|
||||
if isinstance(pending, CompositeFuture):
|
||||
if not pending.resolved:
|
||||
self._emit(WaitCmd(handle=pending.completion))
|
||||
pending.resolved = True
|
||||
return None
|
||||
|
||||
# Composite path (CompletionHandle or None — existing behaviour).
|
||||
self._emit(WaitCmd(handle=handle))
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Phase 1 spec tests for ADR-0065 D8 — composite output handle + output-space DMA.
|
||||
|
||||
`tl.composite(...)` returns the **output TensorHandle** (not a bare
|
||||
CompletionHandle) so its result chains into downstream ops like `tl.dot`'s.
|
||||
`out` is always a handle: `out=tl.ref(addr, shape)` for HBM output (DMA_WRITE
|
||||
inside the composite), `out=<TCM handle>` for in-place, or omitted → TLContext
|
||||
auto-allocates a TCM scratch. The output handle's `space` decides the tile
|
||||
loop's write-back (tcm → STORE only; hbm → STORE + DMA_WRITE). `tl.ref`
|
||||
returns `space="hbm"`.
|
||||
|
||||
Phase 1 (this commit): tests only. FAIL until D8:
|
||||
- `tl.ref` returns space="tcm"; `composite` returns CompletionHandle;
|
||||
output always DMA_WRITEs; `tl.wait` rejects a TensorHandle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from kernbench.common.pe_commands import OpSpec, Scope, TensorHandle
|
||||
from kernbench.triton_emu.tl_context import TLContext
|
||||
|
||||
|
||||
def _tl() -> TLContext:
|
||||
return TLContext(pe_id=0, num_programs=1, scratch_base=0x200000,
|
||||
scratch_size=1 << 20)
|
||||
|
||||
|
||||
def test_ref_returns_hbm_space():
|
||||
h = _tl().ref(0x1000, shape=(4, 4), dtype="f16")
|
||||
assert h.space == "hbm"
|
||||
|
||||
|
||||
def test_composite_returns_handle_auto_tcm():
|
||||
tl = _tl()
|
||||
a = tl.ref(0x1000, shape=(8, 16), dtype="f16")
|
||||
b = tl.ref(0x2000, shape=(16, 8), dtype="f16")
|
||||
out = tl.composite(op="gemm", a=a, b=b) # out omitted → auto TCM
|
||||
assert isinstance(out, TensorHandle)
|
||||
assert out.space == "tcm"
|
||||
assert out.addr >= 0x200000 # auto-allocated scratch
|
||||
assert out.shape == (8, 8)
|
||||
|
||||
|
||||
def test_composite_returns_the_explicit_out_handle():
|
||||
tl = _tl()
|
||||
a = tl.ref(0x1000, shape=(8, 16), dtype="f16")
|
||||
b = tl.ref(0x2000, shape=(16, 8), dtype="f16")
|
||||
O = TensorHandle(id="O", addr=0x5000, shape=(8, 8), dtype="f16",
|
||||
nbytes=128, space="tcm", pinned=True)
|
||||
out = tl.composite(op="gemm", a=a, b=b, out=O)
|
||||
assert out is O
|
||||
|
||||
|
||||
def test_composite_output_chains_into_next_op():
|
||||
tl = _tl()
|
||||
a = tl.ref(0x1000, shape=(8, 16), dtype="f16")
|
||||
b = tl.ref(0x2000, shape=(16, 8), dtype="f16")
|
||||
scores = tl.composite(op="gemm", a=a, b=b) # handle
|
||||
out2 = tl.composite(op="math", a=scores, math_op="exp")
|
||||
assert isinstance(out2, TensorHandle)
|
||||
|
||||
|
||||
def test_wait_accepts_tensor_handle():
|
||||
tl = _tl()
|
||||
a = tl.ref(0x1000, shape=(8, 16), dtype="f16")
|
||||
b = tl.ref(0x2000, shape=(16, 8), dtype="f16")
|
||||
out = tl.composite(op="gemm", a=a, b=b)
|
||||
tl.wait(out) # must not raise
|
||||
|
||||
|
||||
def test_output_space_drives_dma_write():
|
||||
from kernbench.components.builtin.pe_types import StageType
|
||||
from kernbench.components.builtin.tiling import generate_plan_from_ops
|
||||
|
||||
a = TensorHandle(id="a", addr=0x1000, shape=(64, 64), dtype="f16",
|
||||
nbytes=8192, space="tcm", pinned=True)
|
||||
b = TensorHandle(id="b", addr=0x2000, shape=(64, 64), dtype="f16",
|
||||
nbytes=8192, space="hbm")
|
||||
|
||||
def _gemm(out):
|
||||
return OpSpec(kind="gemm", scope=Scope.OUTPUT_TILE,
|
||||
operands={"a": a, "b": b}, out=out,
|
||||
extra={"m": 64, "k": 64, "n": 64})
|
||||
|
||||
o_tcm = TensorHandle(id="ot", addr=0x300000, shape=(64, 64), dtype="f16",
|
||||
nbytes=8192, space="tcm", pinned=True)
|
||||
o_hbm = TensorHandle(id="oh", addr=0x9000, shape=(64, 64), dtype="f16",
|
||||
nbytes=8192, space="hbm")
|
||||
|
||||
def _has_write(ops):
|
||||
plan = generate_plan_from_ops(ops, tile_m=64, tile_k=64, tile_n=64,
|
||||
bytes_per_element=2,
|
||||
pe_prefix="sip0.cube0.pe0")
|
||||
return any(s.stage_type == StageType.DMA_WRITE
|
||||
for s in plan.tiles[0].stages)
|
||||
|
||||
assert not _has_write((_gemm(o_tcm),)), "tcm out → no DMA_WRITE"
|
||||
assert _has_write((_gemm(o_hbm),)), "hbm out → DMA_WRITE"
|
||||
@@ -175,7 +175,10 @@ def test_tl_composite_nonblocking():
|
||||
a = tl.load(0x1000, shape=(32, 64), dtype="f16")
|
||||
b = tl.load(0x2000, shape=(64, 32), dtype="f16")
|
||||
h = tl.composite(op="gemm", a=a, b=b, out_ptr=0x3000)
|
||||
assert isinstance(h, CompletionHandle)
|
||||
# ADR-0065 D8: composite returns the output TensorHandle (chainable),
|
||||
# carrying the completion in its `pending` field.
|
||||
assert isinstance(h, TensorHandle)
|
||||
assert h.addr == 0x3000 and h.space == "hbm"
|
||||
comp_cmds = [c for c in tl.commands if isinstance(c, CompositeCmd)]
|
||||
assert len(comp_cmds) == 1
|
||||
# Flat-ops shape (ADR-0065 D1): head op carries kind + write-back handle.
|
||||
@@ -195,7 +198,8 @@ def test_tl_wait_specific():
|
||||
tl.wait(h)
|
||||
wait_cmds = [c for c in tl.commands if isinstance(c, WaitCmd)]
|
||||
assert len(wait_cmds) == 1
|
||||
assert wait_cmds[0].handle == h
|
||||
# ADR-0065 D8: h is the output handle; tl.wait targets its completion.
|
||||
assert wait_cmds[0].handle == h.pending.completion
|
||||
|
||||
|
||||
# ── 9. tl.wait() → WaitCmd(handle=None) ──────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user