Skip to content

feat(ck-tile): rowcolquant GEMM TE to dispatcher bridge#9979

Open
ozturkosu wants to merge 5 commits into
developfrom
users/muozturk/ck/rowcolquant_bridge_block_scale
Open

feat(ck-tile): rowcolquant GEMM TE to dispatcher bridge#9979
ozturkosu wants to merge 5 commits into
developfrom
users/muozturk/ck/rowcolquant_bridge_block_scale

Conversation

@ozturkosu

Copy link
Copy Markdown
Contributor

Motivation

Adds the gemm_rowcolquant block-scale op (per-row scale on A, per-column scale on B) to
the TileEngine -> Dispatcher bridge (codegen -> C ABI -> Python), so callers can generate,
build, and launch row/col-quant GEMM at parity with Old-TE without writing C++. Uses the
direct-launch, registry-bypass pattern (launch takes ck_tile::QuantGemmHostArgs with
scale pointers the registry backend can't express).

Test Plan

  • CPU unit tests: python3 -m pytest dispatcher/tests/test_rowcolquant_bridge.py -v
  • On-GPU numeric verify vs fp32 reference (tester, serialized on MI300X).
  • clang-format-18 on the ctypes lib.

Test Result

  • CPU unit tests: 9 passed (name prefix, byte-exact codegen<->utils name contract,
    codegen-JSON projection, fp8/bf8-rcr scope, problem defaults).
  • Build (gfx950, hipcc 7.2): fp8 and bf8 compile; fp8 links to a loadable .so
    exporting all 6 dispatcher_* symbols. Name parity byte-exact
    (gemm_rowcolquant_{fp8,bf8}_rcr_compv3_cshuffle_intrawave_16x64x256_1x4x1_16x16x128).
  • clang-format-18: clean on gemm_rowcolquant_ctypes_lib.cpp.
  • On-GPU numeric + perf parity: pending (draft) — posted as a comment after the GPU run.

Scope / known limitations (exact Old-TE match)

  • dtype fp8/bf8, layout rcr only — the exact set gemm_quant_rowcol.cpp registers.
  • Per-row float scale [M,1] on A, per-col float scale [1,N] on B (QK_A=QK_B=1), applied in
    the CShuffle epilogue; QuantType::RowColQuant. Accumulator slot = AccDataType (float),
    narrowed to half only in the epilogue.
  • k_batch == 1 (split-K not registered for rowcol in Old-TE). Non-tile-multiple M/N/K
    rejected by IsSupportedArguments.

Performance parity

Pending — serialized A/B vs Old-TE gemm_rowcolquant, added as a comment after the GPU run.


Related PRs / references (TileEngine -> Dispatcher GEMM bridge series): #8997 (regular), #9000 (grouped), #9028 (stream-K), #8887 (fp8/bf8/int8), #9305 (multi-ABD), #9306 (batched), #9307 (preshuffle), #9308 (multi-D), #9328 (batched-contraction), #9329 (mx_gemm), #9978 (tensor_quant). Sibling block-scale quant bridges (this series): abquant, aquant, bquant, rowcolquant, tensor_quant.

Registry-bypass, single-kernel-per-.so, direct-launch ctypes bridge for the
RowColQuant block-scale GEMM (per-row scale of A + per-column scale of B),
mirroring the grouped_gemm_bquant bridge pattern.

Scope matches Old-TE gemm_quant_rowcol.cpp exactly: fp8/bf8, rcr layout
(RowMajor A / ColMajor B / RowMajor C), compv3 pipeline, intrawave scheduler.
Uses QuantGemmHostArgs with AQ/BQ float scale vectors (QK_A=QK_B=1, no quant
groups). Arch is derived at runtime and throws on unknown (gfx942+gfx950).

Files:
  codegen/unified_gemm_rowcolquant_codegen.py
  bindings/ctypes/gemm_rowcolquant_ctypes_lib.cpp
  python/gemm_rowcolquant_utils.py
  python/rowcolquant_selftest.py
  codegen/codegen_common.py  (make_rowcolquant_kernel_name helper)
  bindings/ctypes/CMakeLists.txt  (dispatcher_gemm_rowcolquant_lib target)
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Reviewer pass (rowcolquant bridge)

Reviewed against Old-TE source of truth example/ck_tile/38_block_scale_gemm/gemm_quant_rowcol.cpp + run_gemm_quant_example.inc on branch users/muozturk/ck/rowcolquant_bridge_block_scale (HEAD 6810179). CPU unit test passes 9/9; clang-format-18 -style=file clean; all 7 files are additive and the shared codegen_common.py edit is append-only (bquant helpers untouched).

Overall this is in good shape and closely mirrors Old-TE. The core correctness traps are handled right. Findings below.

Blocking

None. Parity, correctness, arch handling, registry-bypass, name contract, and CI test are all met (see notes).

Should-fix

  • dispatcher/python/rowcolquant_selftest.py:57-67 + gemm_rowcolquant_utils.py:312-346 — the numeric self-test does not actually validate. RowColQuantGpuGemmRunner.run passes float32 A/B straight through, but the ctypes lib reinterprets the host pointer as const ADataType* (fp8/bf8 = 1 byte) and copies only elements_to_bytes<fp8>(M*K) = M*K bytes from a 4*M*K-byte float32 buffer. The kernel therefore reads the first quarter of the buffer as raw fp8 bytes -> garbage. The _reference also compares against a full-precision float32 matmul with no fp8 rounding. Net: the "max relative error" line is meaningless. Either narrow A/B to real fp8/bf8 bytes before the call (and round the reference the same way) or clearly mark the run path as a smoke test only.
  • **gemm_rowcolquant_utils.py:362-375 (_detect_gpu_arch) + 60 (_DEFAULT_GFX_ARCH="gfx950").** On any detection failure this silently falls back to gfx950, which is a soft "default to a specific arch." The C++ runtime guard (ctypes lib:140-155) correctly rejects unknown archs, so this won't produce wrong results, but a gfx942box with a flaky enumerator would silently build gfx950 objects. Prefer raising, or at leastlog.warning` the fallback, to stay consistent with the "never default an arch" policy.

Nit

  • codegen/unified_gemm_rowcolquant_codegen.py:300ComputeDataType passed as ADataType, whereas Old-TE passes void (AComputeDataType = void since IS_FP8BLOCKSCALE is false for rowcol). These are functionally identical for the registered scope because A==B (fp8/fp8, bf8/bf8) and mixed_prec_compute_type_t<void,A,B> == mixed_prec_compute_type_t<A,A,B> when A==B. No behavior change, but passing void (or a comment) would track Old-TE exactly and avoid a surprise if A!=B variants are ever added.
  • gemm_rowcolquant_ctypes_lib.cpp:161-168 — non-packed strides are rejected. Fine and clearly documented, but note this means stride_B==K is required for the ColMajor B (leading dim K), which is correct for rcr; just calling out that any caller supplying padded leading dims will hit the hard reject.
  • CMake CK_TILE_ROWCOLQUANT_GFX_ARCH default gfx942 (CMakeLists:296-304) mirrors the merged bquant lib and is only a -DGFX_ARCH passthrough string (the source does not static_assert on it, unlike mx_gemm), so it's harmless — consistent with the bquant precedent.

Confirmed correct

  • Parity: exactly {fp8,rcr} + {bf8,rcr}, A=fp8/bf8, B=fp8/bf8, C=half_t, Acc=float; rcr = RowMajor A / ColMajor B / RowMajor C, AQ RowMajor [M,1], BQ ColMajor [1,N] — matches the a_layout=="R" && b_layout=="C" branch. No missing/extra dtype or layout.
  • Correctness: QuantGemmHostArgs with QK_A=QK_B=1, stride_AQ=stride_BQ=1, both scale ptrs set; QuantType::RowColQuant. The accumulator-slot trap is handled: GemmRowColTensorQuantPipelineProblem<ADataType, BDataType, AccDataType, AccDataType, ...> — the CDataType_ slot is float and the real narrowing to half happens in the CShuffleEpilogue, matching run_gemm_quant_example.inc lines 126-133 / 236-252.
  • Arch: runtime hipGetDeviceProperties guard throws on non-gfx942/gfx950; no compile-time gfx942 default in the launch path (matches merged grouped_gemm_bquant).
  • Registry-bypass: direct SelectedKernel::launch(QuantGemmHostArgs, stream_config), dispatcher/include intentionally excluded — matches the bquant template.
  • Name contract: Python .name and codegen KERNEL_NAME both delegate to make_rowcolquant_kernel_name; test locks the contract byte-exact.
  • CI: tests/test_rowcolquant_bridge.py present and meaningful (name prefix, tile encoding, byte-exact contract, codegen-JSON projection, fp8/bf8 rcr scope).
  • split-K: k_batch>1 relies on IsSupportedArgument to reject (Old-TE registers only k_batch=1 for rowcol) — correct.

@therock-pr-bot

therock-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
🌿 Branch Name ✅ Pass
📝 PR Title/Description ✅ Pass
Forbidden Files ✅ Pass
🧪 Unit Test ✅ Pass
🔎 pre-commit ✅ Pass
🚫 Draft PR 🔜 To Be Enabled
🚩 Feature Flag 🔜 To Be Enabled
📊 Code Coverage 🔜 To Be Enabled
🤖 therock-pr-bot ✅ Pass

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

@ozturkosu
ozturkosu requested a review from Copilot July 24, 2026 23:08
@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

Round-2 review fixes for the gemm_rowcolquant dispatcher bridge:

- Self-test now feeds REAL 1-byte-per-element fp8/bf8 bytes to the kernel
  (via ml_dtypes e4m3/e5m2 encode) and rounds the numpy reference through
  the same quantize->dequantize, so "max relative error" is a genuine
  numeric check with a pass/fail tolerance. When ml_dtypes is unavailable
  the run is clearly marked SMOKE-ONLY (no correctness claim).
- _detect_gpu_arch now raises instead of silently falling back to gfx950,
  so a flaky enumerator on a gfx942 box can never build gfx950 objects.
  Explicit gfx_arch= still short-circuits detection.
- Add encode_fp8_bytes / quantize_dequantize_fp8 / fp8_encoding_available
  helpers plus CPU unit tests locking their byte-width and rounding.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-2 fixes

Addressed both should-fix review items on the gemm_rowcolquant bridge (commit 88dae56c43):

#1 — Self-test now actually validates.
The runner previously passed float32 A/B straight through while the ctypes lib reinterprets the pointer as const fp8_t* (1 byte/elem), reading a quarter of the buffer as garbage, and compared against a full-precision numpy reference. Fixed:

  • Added encode_fp8_bytes() (float32 → real 1-byte-per-element OCP fp8 e4m3 / bf8 e5m2 via ml_dtypes) and quantize_dequantize_fp8().
  • rowcolquant_selftest.py now feeds real fp8/bf8 bytes to the kernel and rounds the numpy reference through the same quantize→dequantize, so max relative error is a genuine numeric check with a pass/fail tolerance (0.15).
  • If ml_dtypes is unavailable, the --run path is clearly marked SMOKE-ONLY (kernel launches, timing reported, no correctness claim) and returns without a false pass.

#2 — No more silent arch fallback.
_detect_gpu_arch() now raises RuntimeError when rocm_agent_enumerator fails or returns no usable arch, instead of silently defaulting to gfx950 (which would let a flaky enumerator on a gfx942 box build gfx950 objects). Callers that know their target still pass gfx_arch= to short-circuit detection entirely.

Also added 3 CPU unit tests locking the encoder byte-width (1 byte/elem) and fp8-vs-bf8 rounding.

Tests: pytest dispatcher/tests/test_rowcolquant_bridge.py -v12 passed (9 original + 3 new).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a RowColQuant (per-row A scale + per-col B scale) GEMM path to the CK TileEngine → Dispatcher bridge, enabling codegen → per-kernel .so build → Python ctypes launch for the fp8/bf8 rcr scope using the direct-launch (registry-bypass) pattern.

Changes:

  • Adds a dedicated RowColQuant codegen script and shared kernel-name constructor to keep codegen↔Python naming byte-exact.
  • Introduces a per-kernel ctypes .so (direct SelectedKernel::launch(QuantGemmHostArgs)) plus Python utilities/runner for building and launching.
  • Adds CPU-only unit tests and a Python self-test script for build/run validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
projects/composablekernel/dispatcher/tests/test_rowcolquant_bridge.py New CPU-only unit tests that lock naming contract and config projection for RowColQuant.
projects/composablekernel/dispatcher/python/rowcolquant_selftest.py New self-test CLI to build and optionally run/verify default fp8/bf8 RowColQuant kernels.
projects/composablekernel/dispatcher/python/gemm_rowcolquant_utils.py New Python build utilities + ctypes wrapper + high-level runner for RowColQuant.
projects/composablekernel/dispatcher/codegen/unified_gemm_rowcolquant_codegen.py New code generator producing per-kernel headers defining SelectedKernel + KERNEL_NAME.
projects/composablekernel/dispatcher/codegen/codegen_common.py Adds make_rowcolquant_kernel_name() shared helper to enforce byte-exact naming across layers.
projects/composablekernel/dispatcher/bindings/ctypes/gemm_rowcolquant_ctypes_lib.cpp New ctypes C ABI implementation doing host-pointer allocation/copy and direct kernel launch.
projects/composablekernel/dispatcher/bindings/ctypes/CMakeLists.txt Adds a CMake target for the RowColQuant ctypes library when a generated kernel header is present.

Comment on lines +53 to +60
rng = np.random.default_rng(0)
# fp8/bf8 inputs are represented on the host as float32 here for the
# reference; the kernel's ADataType/BDataType handle narrowing on device.
# For a genuine numeric check, use small integer-ish values.
A = rng.uniform(-2.0, 2.0, size=(M, K)).astype(np.float32)
B = rng.uniform(-2.0, 2.0, size=(K, N)).astype(np.float32)
AQ = rng.uniform(0.5, 1.5, size=(M,)).astype(np.float32)
BQ = rng.uniform(0.5, 1.5, size=(N,)).astype(np.float32)
Comment on lines +248 to +255
A = np.ascontiguousarray(A)
# Kernel BLayout is ColumnMajor (rcr): B[k,n] lives at offset n*K+k.
# Supply column-major bytes for 2-D B; ascontiguousarray would force
# row-major and silently transpose.
B = np.asfortranarray(B) if B.ndim == 2 else np.ascontiguousarray(B)
AQ = np.ascontiguousarray(AQ)
BQ = np.ascontiguousarray(BQ)
C = np.ascontiguousarray(C)
Comment on lines +311 to +323
else()
message(STATUS "No RowColQuant GEMM kernel found for ctypes lib - building without kernel")
add_library(dispatcher_gemm_rowcolquant_lib SHARED gemm_rowcolquant_ctypes_lib.cpp)
target_include_directories(dispatcher_gemm_rowcolquant_lib PRIVATE
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/dispatcher/include
)
target_link_libraries(dispatcher_gemm_rowcolquant_lib PRIVATE hip::device)
set_target_properties(dispatcher_gemm_rowcolquant_lib PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_STANDARD 17
)
endif()
Comment on lines +403 to +409
if variant_key not in ROWCOLQUANT_VARIANTS:
log.warning("Unknown variant_key %s -- skipping", variant_key)
continue
if pipeline not in ROWCOLQUANT_PIPELINE_MAP:
log.warning("Unsupported pipeline %s -- skipping", pipeline)
continue

Round-2 fix: default_fp8_config/default_bf8_config now derive warp_tile_k
from the target arch via _warp_tile_k_for(), mirroring ck_tile
get_k_warp_tile<PrecType,16>(): fp8/bf8 -> 128 on gfx950, 32 on gfx942.

The previous hardcoded warp_tile_k=128 is valid only on gfx950; on gfx942
there is no 16x16x128 fp8/bf8 warp-gemm, so the kernel compiles but
silently outputs all-zeros. This exact bug was confirmed on the sibling
tensor_quant bridge GPU tester, where warp_tile_k=32 (Old-TE's gfx942
value) is bit-exact.

Also updates the byte-exact kernel .name, the codegen default-config note,
and adds CPU tests asserting 32 on gfx942 / 128 on gfx950.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-2 fix: arch-aware warp_tile_k

default_fp8_config / default_bf8_config previously hardcoded warp_tile_k=128 (the ..._16x16x128 name). That is valid only on gfx950: on gfx942 there is no valid 16x16x128 fp8/bf8 warp-gemm, so the kernel compiles but silently outputs all-zeros.

This exact bug was just confirmed on the sibling tensor_quant bridge GPU tester: with warp_tile_k=32 (the correct gfx942 value; Old-TE uses 16x16x32 on gfx942) the output is bit-exact, and with 128 it is all-zeros.

Fix: warp_tile_k is now arch-derived via a new _warp_tile_k_for(variant, gfx_arch) helper that mirrors ck_tile::get_k_warp_tile<PrecType, 16>():

arch fp8/bf8 warp_tile_k
gfx950 128
gfx942 32

Updated: codegen default-config note, the byte-exact .name (now 16x16x32 on gfx942), and the CPU tests (assert 32 on gfx942 / 128 on gfx950).

Verification:

  • python3 -m pytest dispatcher/tests/test_rowcolquant_bridge.py -v -> 17 passed.
  • Smoke-recompiled the gfx942 fp8 kernel with warp_tile_k=32 via hipcc -> .so builds & links (..._16x16x32_gfx942.so).

The rest of the op (fp8/bf8 rcr, per-row A + per-col B scale, AccDataType accumulator, round-2 fp8-encode self-test + arch-raise) is unchanged.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU validation (rowcolquant, MI300X/gfx942)

Node ctr-cx66-mi300x-13 (gfx942, MI300X), HEAD 16ec7c4043, ROCm 7.2. fp8/bf8, rcr, per-row scale A + per-col scale B, k_batch=1.

Arch trap confirmed & round-2 fix works. The gfx942 default correctly resolves to warp_tile_k=32 (...16x16x32). I also built the wrong ...16x16x128 on gfx942 as a control: it produces all-zeros (max_rel≈1.0) — exactly the documented trap. The Python default_{fp8,bf8}_config("gfx942") path avoids it.

Functional correctness (quant→dequant numpy ref, gate 2e-2)

Encoded with FNUZ fp8/bf8 (gfx942 has no CK_USE_OCP_FP8).

dtype shapes tested (M,N,K) max_rel PASS/FAIL
fp8 256³, 512³, 1024³, 2048³, 1024×2048×4096 4.88e-4 PASS
bf8 256³, 512³, 1024³, 2048³, 1024×2048×4096 4.88e-4 PASS

Bridge output is bit-identical to Old-TE (gemm_quant_rowcol, same 16x16x32 instance compiled against the same ctypes lib): max_rel bridge-vs-OldTE = 0.0 on the finite set. Old-TE's own CPU -verify=1 also passes. 100% function parity.

Performance A/B parity vs Old-TE

Identical external timing BOTH sides: same standalone harness, same CK stream_config{warmup=50, repeat=100, gpu_timer, flush_cache=true, rotating_count=1000}, interleaved per (kernel,shape), min of 2 reps.

shape (M,N,K) dtype bridge ms OldTE ms gap%
256,256,512 fp8 0.00380 0.00387 -1.67
512,512,512 fp8 0.00444 0.00433 +2.40
1024,1024,1024 fp8 0.01257 0.01260 -0.26
2048,2048,2048 fp8 0.06461 0.06462 -0.03
4096,4096,4096 fp8 0.41702 0.41704 -0.00
1024,2048,4096 fp8 0.06271 0.06274 -0.04
3840,4096,2048 fp8 0.21365 0.21293 +0.34
256,256,512 bf8 0.00387 0.00380 +2.01
512,512,512 bf8 0.00432 0.00448 -3.40
1024,1024,1024 bf8 0.01254 0.01255 -0.04
2048,2048,2048 bf8 0.06505 0.06501 +0.06
4096,4096,4096 bf8 0.41734 0.41723 +0.03
1024,2048,4096 bf8 0.06286 0.06332 -0.73
3840,4096,2048 bf8 0.21289 0.21257 +0.15

median gap = -0.02%, 100% within ±15% (14/14), max |gap| = 3.40% (small shapes only — measurement noise). No >15% rows.

Verdict

PASS — functional + performance at 100% Old-TE parity on gfx942 for fp8 and bf8. Ready from a validation standpoint.

Non-blocking note for a coder

  1. rowcolquant_selftest.py main() calls default_{fp8,bf8}_config() without the arch arg, so it builds the gfx950 16x16x128 tile even under --arch gfx942 (produces all-zeros). It should thread the resolved arch into the config (default_fp8_config(args.arch)).
  2. gemm_rowcolquant_utils.encode_fp8_bytes/quantize_dequantize_fp8 hardcode OCP float8_e4m3/e5m2. On gfx942 (ck_tile::fp8_t=e4m3fnuz) this silently yields NaN/garbage in the self-test verify. It should pick FNUZ vs OCP by arch (mirror CK_USE_OCP_FP8). Kernel itself is correct with FNUZ.

Round-3 GPU-tester findings (self-test/reference path only; shipping
kernel is confirmed Old-TE bit-exact + perf parity):

1. Arch not threaded: rowcolquant_selftest main() called
   default_fp8_config()/default_bf8_config() with no arch, so --arch gfx942
   still built the gfx950 16x16x128 tile -> all-zeros. Resolve arch once
   (arg or autodetect) and pass it to both configs and the encoders.

2. FP8 encoding was OCP-only: encode_fp8_bytes/quantize_dequantize_fp8
   hardcoded OCP float8_e4m3/e5m2, so the numpy reference silently NaN'd on
   gfx942 (which uses FNUZ fp8_t). Select FNUZ vs OCP by arch, mirroring
   CK_USE_OCP_FP8 (gfx942->FNUZ, gfx950/gfx12->OCP), via ml_dtypes
   e4m3fnuz/e5m2fnuz. SMOKE-ONLY fallback when ml_dtypes is absent kept.

Add tests asserting the arch threads into the default config and that the
encoder picks FNUZ on gfx942 / OCP on gfx950. 23/23 pass.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-3: self-test arch + FNUZ fixes

Addresses the two non-blocking self-test/reference bugs the GPU tester flagged (the shipping kernel itself is confirmed Old-TE bit-exact + perf within ±15% — these were reference-path-only issues). Fixed in 5d8ad1930a.

  1. Arch not threaded into the default config. rowcolquant_selftest.py main() called default_fp8_config() / default_bf8_config() with no arch, so --arch gfx942 still built the gfx950 16x16x128 tile → all-zeros. Now the arch is resolved once (explicit --arch or autodetect) and threaded into both configs and the encoders.

  2. FP8 encoding was OCP-only. encode_fp8_bytes / quantize_dequantize_fp8 hardcoded OCP float8_e4m3 / e5m2, so on gfx942 (where ck_tile::fp8_t is FNUZ) the numpy reference silently NaN'd. The encoder now selects FNUZ vs OCP by arch, mirroring CK_USE_OCP_FP8 (gfx942 → e4m3fnuz/e5m2fnuz, gfx950/gfx12 → OCP), via ml_dtypes. The SMOKE-ONLY fallback when ml_dtypes is absent is preserved.

Tests: added TestArchThreadedIntoConfig (arch reaches warp_tile_k + kernel name) and TestFp8EncodingFlavourByArch (encoder picks FNUZ on gfx942 / OCP on gfx950; FNUZ round-trip stays finite). python3 -m pytest dispatcher/tests/test_rowcolquant_bridge.py -v23/23 pass.

@ozturkosu
ozturkosu marked this pull request as ready for review July 25, 2026 06:02
@ozturkosu
ozturkosu requested a review from a team as a code owner July 25, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants