Skip to content

[Refactor] Refactor CUDA atomic helpers - #2001

Merged
SiriusNEO merged 2 commits into
tile-ai:mainfrom
SiriusNEO:chaofan/atomic_0330
Mar 31, 2026
Merged

SiriusNEO merged 2 commits into
tile-ai:mainfrom
SiriusNEO:chaofan/atomic_0330

Conversation

@SiriusNEO

@SiriusNEO SiriusNEO commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

This PR refactors src/tl_templates/cuda/atomic.h to reduce repeated inline PTX code and make the atomic add paths easier to maintain.

Summary by CodeRabbit

  • Refactor
    • Consolidated CUDA atomic implementations for floating-point types (fp16, bf16, f32) to reduce duplication.
    • Unified memory-order handling so non-relaxed atomics follow consistent ordering semantics.
    • Streamlined vectorized atomic operations across widths (x2, x4) with scalar fallbacks for older architectures.
    • Broadened availability of vector conversion utilities used by atomic paths.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the TileLang project.

Please remember to run pre-commit run --all-files in the root directory of the project to ensure your changes are properly linted and formatted. This will help ensure your contribution passes the format check.

We appreciate you taking this step! Our team will review your contribution, and we look forward to your awesome work! 🚀

@coderabbitai

coderabbitai Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4cef157d-9902-43a6-9cf2-ab0652c55b96

📥 Commits

Reviewing files that changed from the base of the PR and between 4ae50e1 and 46f08d2.

📒 Files selected for processing (1)
  • src/tl_templates/cuda/atomic.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tl_templates/cuda/atomic.h

📝 Walkthrough

Walkthrough

Introduces memory-order classification helpers, 16-bit pack/unpack utilities, and centralized PTX inline-assembly atomic-add helpers; refactors fp16/bf16 and vectorized atomic-add implementations to use PTX helpers with scalar fallbacks for older architectures. No public signatures changed.

Changes

Cohort / File(s) Summary
CUDA atomic implementation
src/tl_templates/cuda/atomic.h
Added tl_atomic_detail memory-order classifiers, PackBits16/UnpackBits16, centralized PTX atomic-add helpers for fp16/bf16 and vector widths (v2/v4), refactored AtomicAdd/AtomicAddRet and AtomicAddx2/AtomicAddx2Ret/AtomicAddx4/AtomicAddx4Ret to call helpers, and added scalar pair/quad fallback helpers for non-PTX/old-arch paths.

Sequence Diagram(s)

sequenceDiagram
  participant Thread
  participant PTX_Helper as PTX Helper (inline asm)
  participant Scalar_Fallback as Scalar Fallback (per-element atomicAdd)
  participant Memory

  Thread->>PTX_Helper: if memory_order != relaxed and arch supports PTX
  alt PTX path
    PTX_Helper->>Memory: perform grouped atomic-add (fp16/bf16/v2/v4)
    Memory-->>PTX_Helper: updated value (if returning)
    PTX_Helper-->>Thread: result/ack
  else scalar fallback
    Thread->>Scalar_Fallback: call pair/quad scalar helper
    Scalar_Fallback->>Memory: perform element-wise atomicAdd
    Memory-->>Scalar_Fallback: per-element results
    Scalar_Fallback-->>Thread: combined result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • LeiWang1999
  • Rachmanino

Poem

🐰✨ I hopped through bits and packed them tight,
Half and bfloat danced in PTX light.
Helpers gather orders, neat and small,
Fallbacks stitch when PTX can't call —
A rabbit’s hop makes atomics right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[Refactor] Refactor CUDA atomic helpers' directly and specifically summarizes the main change: refactoring atomic helper functions in CUDA code to reduce duplication.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tl_templates/cuda/atomic.h`:
- Around line 56-68: Update the memory-order classifier functions so they match
C++/CUDA semantics and allow the PTX opcode selection to handle consume and
seq_cst specially: change IsReleaseLikeMemoryOrder to exclude
memory_order_consume (only treat memory_order_release as release-like), ensure
IsAcquireMemoryOrder treats memory_order_acquire and memory_order_consume as
acquire-like, and ensure IsAcqRelLikeMemoryOrder does not include
memory_order_seq_cst (only include memory_order_acq_rel); then split handling
for memory_order_consume and memory_order_seq_cst into dedicated branches before
the PTX opcode selection so seq_cst uses the required fence.sc wrapper per the
PTX atomic ABI and consume uses acquire-like semantics.
- Around line 98-116: The BF16 PTX helper AtomicAddPtxBF16 is unguarded and can
emit SM90-only PTX; wrap its definition and any call sites (notably the
return-value caller AtomicAddRet) with the same SM90+ preprocessor guard used
for the void AtomicAdd overload (use `#if` (defined(__CUDA_ARCH_LIST__) &&
(__CUDA_ARCH_LIST__ > 890)) / `#endif`) so PTX instructions like atom.*.bf16 are
only compiled for SM90+, or provide an alternative non-PTX fallback inside the
guarded branch for SM8.x; ensure the symbol AtomicAddPtxBF16 and its use in
AtomicAddRet are enclosed by this guard.
- Around line 118-160: AtomicAddx2 and AtomicAddx2Ret currently call SM90-only
PTX helpers (AtomicAddPtxV2F16 / AtomicAddPtxV2BF16) for non-relaxed orders with
no SM guards, which breaks pre-SM90 (and pre-SM80 for bf16) targets; add
compile-time SM guards and provide fallback ordered implementations: wrap the
SM90 PTX calls in a __CUDA_ARCH__ >= 900 check for v2.f16 (and >= 800/750 as
appropriate for bf16), and in the else branch implement ordered behavior by
performing two lane-wise ordered scalar atomic adds (e.g., call the existing
scalar atomic-add/AtomicAddPtx for each half/bf16 lane or loop with
compare-and-swap to emulate acquire/release semantics) so AtomicAddx2 and
AtomicAddx2Ret work correctly on older SMs. Ensure you modify the functions
AtomicAddx2 / AtomicAddx2Ret to choose the guarded PTX path or the
scalar/emulated fallback, and keep existing relaxed path unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1bd03053-6bd4-4a91-a4ea-3d0c788f043a

📥 Commits

Reviewing files that changed from the base of the PR and between 1c561f6 and 4ae50e1.

📒 Files selected for processing (1)
  • src/tl_templates/cuda/atomic.h

Comment thread src/tl_templates/cuda/atomic.h
Comment thread src/tl_templates/cuda/atomic.h Outdated
Comment on lines +98 to +116
TL_DEVICE void AtomicAddPtxBF16(unsigned short &ret, unsigned long long addr,
unsigned short val, int memory_order) {
if (IsReleaseLikeMemoryOrder(memory_order)) {
asm volatile("atom.release.gpu.global.add.noftz.bf16 %0, [%1], %2;"
: "=h"(ret)
: "l"(addr), "h"(val)
: "memory");
} else if (IsAcquireMemoryOrder(memory_order)) {
asm volatile("atom.acquire.gpu.global.add.noftz.bf16 %0, [%1], %2;"
: "=h"(ret)
: "l"(addr), "h"(val)
: "memory");
} else if (IsAcqRelLikeMemoryOrder(memory_order)) {
asm volatile("atom.acq_rel.gpu.global.add.noftz.bf16 %0, [%1], %2;"
: "=h"(ret)
: "l"(addr), "h"(val)
: "memory");
}
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure
fd -t f atomic.h | head -20

Repository: tile-ai/tilelang

Length of output: 120


🏁 Script executed:

# Get the full content of the atomic.h file to understand the context
wc -l src/tl_templates/cuda/atomic.h

Repository: tile-ai/tilelang

Length of output: 94


🏁 Script executed:

# Read the file around the mentioned lines and search for calls to AtomicAddPtxBF16
cat -n src/tl_templates/cuda/atomic.h | head -150

Repository: tile-ai/tilelang

Length of output: 6450


🏁 Script executed:

# Search for calls to AtomicAddPtxBF16 and references to SM90
rg -n "AtomicAddPtxBF16|SM90|sm_90" src/tl_templates/cuda/atomic.h -A 2 -B 2

Repository: tile-ai/tilelang

Length of output: 825


🏁 Script executed:

# Read context around line 413
cat -n src/tl_templates/cuda/atomic.h | sed -n '400,430p'

Repository: tile-ai/tilelang

Length of output: 1443


🏁 Script executed:

# Read context around line 454
cat -n src/tl_templates/cuda/atomic.h | sed -n '440,470p'

Repository: tile-ai/tilelang

Length of output: 1535


🏁 Script executed:

# Look for SM90 or CUDA_ARCH guards in the file
rg -n "__CUDA_ARCH|SM90|sm_90|cuda_arch_list" src/tl_templates/cuda/atomic.h -i

Repository: tile-ai/tilelang

Length of output: 398


🏁 Script executed:

# Read context around line 388 to see which function has SM90 guard
cat -n src/tl_templates/cuda/atomic.h | sed -n '380,420p'

Repository: tile-ai/tilelang

Length of output: 1942


🏁 Script executed:

# Check which functions are wrapped by SM90 guards
cat -n src/tl_templates/cuda/atomic.h | sed -n '375,430p'

Repository: tile-ai/tilelang

Length of output: 2584


🏁 Script executed:

# Read more context to see the function with return value
cat -n src/tl_templates/cuda/atomic.h | sed -n '430,480p'

Repository: tile-ai/tilelang

Length of output: 2507


🏁 Script executed:

# Check if there's a guard around the return value function that contains line 454
cat -n src/tl_templates/cuda/atomic.h | sed -n '430,468p'

Repository: tile-ai/tilelang

Length of output: 1944


🏁 Script executed:

# Check if AtomicAddRet is guarded by any SM version check
cat -n src/tl_templates/cuda/atomic.h | grep -B 5 "TL_DEVICE T1 AtomicAddRet"

Repository: tile-ai/tilelang

Length of output: 262


🏁 Script executed:

# Verify the complete structure - check if there are any guards around AtomicAddRet
cat -n src/tl_templates/cuda/atomic.h | sed -n '420,435p'

Repository: tile-ai/tilelang

Length of output: 812


🌐 Web query:

PTX atom.add.noftz.bf16 SM90 requirement NVIDIA documentation

💡 Result:

The PTX instruction atom.add.noftz.bf16 (bfloat16 atomic add with no flush-to-zero) requires NVIDIA Hopper architecture (compute capability SM_90) or higher. This is the native hardware atomic add operation for bfloat16 data type in global or shared memory. On pre-SM_90 architectures (e.g., Ampere SM_80), CUDA's atomicAdd for bfloat16 falls back to a software emulation using atomic compare-and-swap (CAS) loops combined with fma.rn.bf16 operations, rather than a direct atomic add instruction. PTX ISA support for bf16 atomics was introduced in version 7.8, coinciding with SM_90.

Citations:


Guard the scalar BF16 PTX helper to SM90+.

AtomicAddPtxBF16 (lines 98–116) contains PTX instructions like atom.release.gpu.global.add.noftz.bf16 that require SM90+. However, this function is called unconditionally in AtomicAddRet (line 454) when handling non-relaxed memory orders with BF16 types, without any SM version guard. On SM8.x devices, this will attempt to execute unsupported PTX instructions and fail at runtime. The void AtomicAdd overload at line 388 correctly guards this with #if (defined(__CUDA_ARCH_LIST__) && (__CUDA_ARCH_LIST__ > 890)), but the return-value version has no such protection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tl_templates/cuda/atomic.h` around lines 98 - 116, The BF16 PTX helper
AtomicAddPtxBF16 is unguarded and can emit SM90-only PTX; wrap its definition
and any call sites (notably the return-value caller AtomicAddRet) with the same
SM90+ preprocessor guard used for the void AtomicAdd overload (use `#if`
(defined(__CUDA_ARCH_LIST__) && (__CUDA_ARCH_LIST__ > 890)) / `#endif`) so PTX
instructions like atom.*.bf16 are only compiled for SM90+, or provide an
alternative non-PTX fallback inside the guarded branch for SM8.x; ensure the
symbol AtomicAddPtxBF16 and its use in AtomicAddRet are enclosed by this guard.

Comment thread src/tl_templates/cuda/atomic.h Outdated
@SiriusNEO

Copy link
Copy Markdown
Collaborator Author

@regression-perf

@github-actions

Copy link
Copy Markdown

Performance Regression Test Report

Triggered by: @SiriusNEO
Workflow run: https://github.com/tile-ai/tilelang/actions/runs/23795459342

Results

File Original Latency Current Latency Speedup
tilelang_example_sparse_tensorcore 0.014445 0.0379029 0.381105
example_vertical_slash_sparse_attn 0.226287 0.5388 0.419984
example_warp_specialize_gemm_copy_0_gemm_1 0.0384917 0.0839244 0.458648
example_mha_fwd_varlen 0.0450062 0.0673579 0.668165
example_tilelang_gemm_fp8_2xAcc 0.182313 0.187461 0.972539
example_mhc_pre 0.146011 0.149604 0.97598
example_tilelang_gemm_fp8_intrinsic 0.809274 0.828958 0.976255
example_warp_specialize_gemm_softpipe_stage2 0.0264674 0.0269222 0.98311
example_warp_specialize_gemm_copy_1_gemm_0 0.0264615 0.0269099 0.983336
example_linear_attn_bwd 0.149288 0.151286 0.986794
block_sparse_attn_tilelang 0.00910918 0.00922573 0.987367
example_tilelang_gemm_splitk_vectorize_atomicadd 1.08532 1.09762 0.988797
example_tilelang_gemm_fp8 0.307846 0.311136 0.989426
example_topk 0.0108749 0.0109818 0.990257
example_tilelang_gemm_splitk 1.07634 1.08691 0.990271
example_fusedmoe_tilelang 0.1311 0.132386 0.990283
example_mha_fwd_bshd 0.025308 0.0255076 0.992174
example_linear_attn_fwd 0.0363553 0.0365557 0.99452
example_mha_inference 0.0773432 0.0777593 0.99465
example_per_token_cast_to_fp8 0.00734212 0.00737807 0.995126
example_blocksparse_gemm 0.019763 0.0198443 0.995907
example_mha_sink_bwd_bhsd_sliding_window 0.0438464 0.0439537 0.997558
example_dynamic 0.637902 0.639463 0.997559
example_warp_specialize_gemm_barrierpipe_stage2 0.0394041 0.0394806 0.998062
example_dequant_gemm_bf16_fp4_hopper 0.550558 0.551552 0.998198
example_dequant_gemm_bf16_mxfp4_hopper 0.505904 0.506312 0.999194
topk_selector 0.0530148 0.0530508 0.999321
example_dequant_gemv_fp16xint4 0.0282164 0.0282333 0.999401
example_gqa_bwd 0.0498291 0.0498571 0.999439
example_convolution_autotune 0.980039 0.980448 0.999582
example_tilelang_nsa_fwd 0.00680918 0.00681136 0.99968
example_tilelang_block_sparse_attn 0.00872973 0.00873142 0.999806
example_mla_decode 0.442471 0.442544 0.999836
example_dequant_gemm_fp4_hopper 1.02507 1.02511 0.999962
example_tilelang_sparse_gqa_decode_varlen_indice 0.0160325 0.0160328 0.999985
example_gqa_bwd_tma_reduce_varlen 0.0515353 0.0515337 1.00003
sparse_mla_bwd 0.413684 0.413662 1.00005
example_group_per_split_token_cast_to_fp8 0.0103167 0.0103158 1.00008
fp8_lighting_indexer 0.0354579 0.0354536 1.00012
example_dequant_gemm_w4a8 5.23037 5.22972 1.00013
example_convolution 1.27224 1.27207 1.00014
example_mhc_post 0.109048 0.109028 1.00018
example_tilelang_sparse_gqa_decode_varlen_mask 0.0174427 0.0174366 1.00035
example_mha_sink_fwd_bhsd_sliding_window 0.0155759 0.0155672 1.00056
sparse_mla_fwd 0.129328 0.129243 1.00066
sparse_mla_fwd_pipelined 0.0949677 0.0948952 1.00076
example_tilelang_nsa_decode 0.00736093 0.00735149 1.00128
example_mha_sink_fwd_bhsd 0.0151803 0.015155 1.00167
example_gqa_fwd_bshd 0.0691664 0.0690387 1.00185
example_gqa_decode 0.0482421 0.0481473 1.00197
example_mha_bwd_bhsd 0.0382992 0.0382154 1.00219
example_elementwise_add 0.115892 0.115621 1.00235
example_mha_sink_bwd_bhsd 0.0616035 0.0613214 1.0046
example_gqa_sink_bwd_bhsd_sliding_window 0.0252468 0.0251029 1.00573
example_gemv 0.286559 0.284864 1.00595
example_mha_bwd_bshd 0.0387692 0.0385357 1.00606
example_gqa_sink_bwd_bhsd 0.0408544 0.0405203 1.00825
example_dequant_groupedgemm_bf16_mxfp4_hopper 3.44809 3.40607 1.01234
example_mha_fwd_bhsd 0.0109998 0.0106977 1.02824

Artifacts

  • regression_result.png (speedup plot) is attached as a workflow artifact. Download it from the workflow run page above.

@SiriusNEO

Copy link
Copy Markdown
Collaborator Author

@regression-perf

@github-actions

Copy link
Copy Markdown

Performance Regression Test Report

Triggered by: @SiriusNEO
Workflow run: https://github.com/tile-ai/tilelang/actions/runs/23797627588

Results

File Original Latency Current Latency Speedup
example_blocksparse_gemm 0.0199391 0.0200325 0.995336
example_gqa_bwd_tma_reduce_varlen 0.0522968 0.0524154 0.997736
example_mha_sink_fwd_bhsd_sliding_window 0.0157074 0.015739 0.997997
example_tilelang_gemm_splitk_vectorize_atomicadd 1.09591 1.09799 0.9981
example_dequant_gemm_fp4_hopper 1.0504 1.05238 0.998122
example_mha_sink_bwd_bhsd 0.0621005 0.0622133 0.998186
example_convolution 1.29385 1.29574 0.998538
example_warp_specialize_gemm_softpipe_stage2 0.0269048 0.0269258 0.999221
tilelang_example_sparse_tensorcore 0.0145883 0.0145982 0.999317
example_topk 0.010985 0.0109918 0.999387
example_elementwise_add 0.115753 0.115819 0.999426
example_tilelang_gemm_fp8_intrinsic 0.834464 0.834933 0.999438
example_mha_fwd_bshd 0.0257445 0.0257558 0.999563
sparse_mla_bwd 0.420588 0.420734 0.999653
example_tilelang_nsa_decode 0.00739385 0.00739632 0.999667
example_tilelang_nsa_fwd 0.00684553 0.00684756 0.999703
example_mla_decode 0.454311 0.45432 0.99998
example_fusedmoe_tilelang 0.132396 0.132394 1.00001
example_dequant_gemm_bf16_fp4_hopper 0.563231 0.563193 1.00007
example_dynamic 0.643288 0.643243 1.00007
example_linear_attn_fwd 0.0365498 0.0365454 1.00012
example_gemv 0.284874 0.284834 1.00014
example_tilelang_gemm_fp8_2xAcc 0.187456 0.187423 1.00017
example_linear_attn_bwd 0.151245 0.151216 1.0002
example_tilelang_gemm_fp8 0.311637 0.311574 1.0002
example_warp_specialize_gemm_copy_1_gemm_0 0.0269201 0.0269144 1.00021
example_dequant_gemm_bf16_mxfp4_hopper 0.514786 0.514674 1.00022
example_convolution_autotune 0.983195 0.982934 1.00027
example_mhc_post 0.108944 0.108908 1.00033
example_group_per_split_token_cast_to_fp8 0.0103481 0.0103437 1.00043
sparse_mla_fwd 0.130851 0.130791 1.00046
example_warp_specialize_gemm_copy_0_gemm_1 0.0388178 0.0387949 1.00059
example_gqa_fwd_bshd 0.0702517 0.0702101 1.00059
example_vertical_slash_sparse_attn 0.231173 0.231031 1.00062
example_per_token_cast_to_fp8 0.00736501 0.00735719 1.00106
example_gqa_sink_bwd_bhsd 0.0414047 0.0413583 1.00112
example_gqa_sink_bwd_bhsd_sliding_window 0.0255712 0.0255386 1.00128
example_mha_fwd_bhsd 0.0107965 0.0107814 1.0014
example_warp_specialize_gemm_barrierpipe_stage2 0.0397415 0.0396837 1.00146
example_tilelang_block_sparse_attn 0.00879151 0.00877489 1.00189
example_mha_fwd_varlen 0.0454576 0.0453653 1.00204
example_mhc_pre 0.152527 0.152206 1.00211
example_tilelang_gemm_splitk 1.09316 1.08993 1.00296
example_mha_sink_bwd_bhsd_sliding_window 0.0444199 0.0442856 1.00303
example_tilelang_sparse_gqa_decode_varlen_indice 0.0162563 0.0161875 1.00425
example_tilelang_sparse_gqa_decode_varlen_mask 0.0177073 0.0176057 1.00577
example_mha_bwd_bhsd 0.0391057 0.0388681 1.00611
block_sparse_attn_tilelang 0.00941194 0.00918683 1.0245
example_mha_sink_fwd_bhsd 0.0164557 0.0153052 1.07517
sparse_mla_fwd_pipelined 0.109538 0.0959764 1.1413
example_mha_bwd_bshd 0.0484008 0.0391078 1.23763
example_gqa_decode 0.0941935 0.0481769 1.95516
fp8_lighting_indexer 0.0711066 0.0357977 1.98635
example_dequant_gemm_w4a8 11.2691 5.35391 2.10484
topk_selector 0.112797 0.0534831 2.10902
example_dequant_groupedgemm_bf16_mxfp4_hopper 7.68473 3.48744 2.20354
example_mha_inference 0.172604 0.0781603 2.20833
example_dequant_gemv_fp16xint4 0.0828232 0.0282648 2.93027
example_gqa_bwd 0.172395 0.0506875 3.40113

Artifacts

  • regression_result.png (speedup plot) is attached as a workflow artifact. Download it from the workflow run page above.

@SiriusNEO
SiriusNEO merged commit eb6f05c into tile-ai:main Mar 31, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant