Here is my solution:
function getProductCarousel($atts) {
$productIds = explode(',',$atts['product_ids']);
?>
<div class="col-center m-auto">
<div id="myCarousel" class="carousel slide" data-ride="carousel" data-interval="0">
<!-- Carousel indicators -->
<ol class="carousel-indicators">
<?php $slide = 0; ?>
<?php foreach($productIds as $index => $productId) { ?>
<?php if ($index % 3 == 0) { ?>
<li data-target="#myCarousel" data-slide-to="<?= $slide++; ?>" <?= $index == 0 ? 'class="active"': ''; ?>></li>
<?php } ?>
<?php } ?>
</ol>
<!-- Wrapper for carousel items -->
<div class="carousel-inner">
<div class="item carousel-item active">
<div class="row">
<?php foreach($productIds as $index => $productId) { ?>
<?php if ($index % 3 == 0 && $index > 0) { ?>
</div></div>
<div class="item carousel-item"><div class="row">
<?php } ?>
<?= getSingleProduct(['id' => $productId]); ?>
<?php } ?>
</div>
</div>
</div>
<!-- Carousel controls -->
<a class="carousel-control left carousel-control-prev" href="#myCarousel" data-slide="prev">
<i class="fa fa-angle-left"></i>
</a>
<a class="carousel-control right carousel-control-next" href="#myCarousel" data-slide="next">
<i class="fa fa-angle-right"></i>
</a>
</div>
</div>
<?php
}