mirror of
https://github.com/NVIDIA/dgx-spark-playbooks.git
synced 2026-04-23 02:23:53 +00:00
571 lines
16 KiB
TypeScript
571 lines
16 KiB
TypeScript
// Remote WebGPU Clustering Client
|
|
// Provides fallback clustering when local WebGPU is not available
|
|
|
|
export interface RemoteClusteringOptions {
|
|
serviceUrl: string;
|
|
mode: 'hybrid' | 'webrtc_stream';
|
|
clusterDimensions: [number, number, number];
|
|
forceSimulation: boolean;
|
|
maxIterations: number;
|
|
webrtcOptions?: {
|
|
autoRefresh: boolean;
|
|
refreshInterval: number;
|
|
};
|
|
|
|
// Semantic clustering options
|
|
clusteringMethod?: string; // "spatial", "semantic", "hybrid"
|
|
semanticAlgorithm?: string; // "hierarchical", "kmeans", "dbscan"
|
|
numberOfClusters?: number | null;
|
|
similarityThreshold?: number;
|
|
nameWeight?: number;
|
|
contentWeight?: number;
|
|
spatialWeight?: number;
|
|
}
|
|
|
|
export interface ClusteringResult {
|
|
clusteredNodes: any[];
|
|
clusterInfo: {
|
|
totalClusters: number;
|
|
usedClusters: number;
|
|
clusterDimensions: [number, number, number];
|
|
processingTime: number;
|
|
gpuAccelerated: boolean;
|
|
clusterStats?: any;
|
|
};
|
|
processingTime: number;
|
|
mode: string;
|
|
sessionId?: string;
|
|
}
|
|
|
|
export interface ServiceCapabilities {
|
|
modes: {
|
|
hybrid: {
|
|
available: boolean;
|
|
description: string;
|
|
};
|
|
webrtc_stream: {
|
|
available: boolean;
|
|
description: string;
|
|
};
|
|
};
|
|
gpuAcceleration: {
|
|
rapidsAvailable: boolean;
|
|
opencvAvailable: boolean;
|
|
plottingAvailable: boolean;
|
|
};
|
|
clusterDimensions: [number, number, number];
|
|
maxClusterCount: number;
|
|
}
|
|
|
|
/**
|
|
* Remote WebGPU Clustering Client
|
|
* Provides GPU-accelerated clustering for browsers without WebGPU support
|
|
*/
|
|
export class RemoteWebGPUClusteringClient {
|
|
private serviceUrl: string;
|
|
private useProxy: boolean;
|
|
private websocket: WebSocket | null = null;
|
|
private capabilities: ServiceCapabilities | null = null;
|
|
private eventListeners: Map<string, Function[]> = new Map();
|
|
|
|
constructor(serviceUrl: string = 'http://localhost:8083', useProxy: boolean = false) {
|
|
this.serviceUrl = serviceUrl;
|
|
this.useProxy = useProxy;
|
|
}
|
|
|
|
private getApiUrl(path: string): string {
|
|
if (this.useProxy) {
|
|
// Use the Next.js API proxy route
|
|
return `/api/remote-webgpu/${path}`;
|
|
} else {
|
|
// Direct connection to service
|
|
return `${this.serviceUrl}/${path}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the remote service is available and get its capabilities
|
|
*/
|
|
async checkAvailability(): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(this.getApiUrl('api/capabilities'));
|
|
if (response.ok) {
|
|
this.capabilities = await response.json();
|
|
console.log('Remote WebGPU service available:', this.capabilities);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.warn('Remote WebGPU service not available:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get service capabilities
|
|
*/
|
|
getCapabilities(): ServiceCapabilities | null {
|
|
return this.capabilities;
|
|
}
|
|
|
|
/**
|
|
* Perform remote clustering
|
|
*/
|
|
async clusterNodes(
|
|
nodes: any[],
|
|
links: any[],
|
|
options: Partial<RemoteClusteringOptions> = {}
|
|
): Promise<ClusteringResult> {
|
|
const requestOptions: RemoteClusteringOptions = {
|
|
serviceUrl: this.serviceUrl,
|
|
mode: 'hybrid',
|
|
clusterDimensions: [32, 18, 24],
|
|
forceSimulation: true,
|
|
maxIterations: 100,
|
|
...options
|
|
};
|
|
|
|
const requestData = {
|
|
graph_data: {
|
|
nodes,
|
|
links
|
|
},
|
|
mode: requestOptions.mode,
|
|
cluster_dimensions: requestOptions.clusterDimensions,
|
|
force_simulation: requestOptions.forceSimulation,
|
|
max_iterations: requestOptions.maxIterations,
|
|
webrtc_options: requestOptions.webrtcOptions,
|
|
|
|
// Semantic clustering parameters
|
|
clustering_method: requestOptions.clusteringMethod || "hybrid",
|
|
semantic_algorithm: requestOptions.semanticAlgorithm || "hierarchical",
|
|
n_clusters: requestOptions.numberOfClusters,
|
|
similarity_threshold: requestOptions.similarityThreshold || 0.7,
|
|
name_weight: requestOptions.nameWeight || 0.6,
|
|
content_weight: requestOptions.contentWeight || 0.3,
|
|
spatial_weight: requestOptions.spatialWeight || 0.1
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(this.getApiUrl('api/cluster'), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Remote clustering failed: ${response.statusText}`);
|
|
}
|
|
|
|
const rawResult = await response.json();
|
|
|
|
// Map snake_case API response to camelCase interface
|
|
const result: ClusteringResult = {
|
|
clusteredNodes: rawResult.clustered_nodes || [],
|
|
clusterInfo: {
|
|
totalClusters: rawResult.total_clusters || 0,
|
|
usedClusters: rawResult.used_clusters || 0,
|
|
clusterDimensions: rawResult.cluster_dimensions || [32, 18, 24],
|
|
processingTime: rawResult.processing_time || 0,
|
|
gpuAccelerated: rawResult.gpu_accelerated || true,
|
|
clusterStats: rawResult.cluster_stats
|
|
},
|
|
processingTime: rawResult.processing_time || 0,
|
|
sessionId: rawResult.session_id,
|
|
mode: rawResult.mode
|
|
};
|
|
|
|
console.log('🔄 Mapped API response:', {
|
|
originalKeys: Object.keys(rawResult),
|
|
mappedClusteredNodes: result.clusteredNodes?.length,
|
|
processingTime: result.processingTime
|
|
});
|
|
|
|
// Emit clustering complete event
|
|
this.emit('clusteringComplete', result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Remote clustering request failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start WebRTC streaming session
|
|
*/
|
|
async startWebRTCStreaming(nodes: any[], links: any[]): Promise<string | null> {
|
|
const result = await this.clusterNodes(nodes, links, { mode: 'webrtc_stream' });
|
|
|
|
if (result.sessionId) {
|
|
console.log(`WebRTC streaming session started: ${result.sessionId}`);
|
|
return result.sessionId;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get WebRTC stream frame URL
|
|
*/
|
|
getStreamFrameUrl(sessionId: string): string {
|
|
if (this.useProxy) {
|
|
return `/api/remote-webgpu-stream/${sessionId}`;
|
|
} else {
|
|
return `${this.serviceUrl}/api/stream/${sessionId}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup WebRTC streaming session
|
|
*/
|
|
async cleanupWebRTCSession(sessionId: string): Promise<void> {
|
|
try {
|
|
await fetch(this.getApiUrl(`api/stream/${sessionId}`), {
|
|
method: 'DELETE'
|
|
});
|
|
console.log(`WebRTC session ${sessionId} cleaned up`);
|
|
} catch (error) {
|
|
console.warn(`Failed to cleanup WebRTC session ${sessionId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect to WebSocket for real-time updates
|
|
*/
|
|
connectWebSocket(): void {
|
|
if (this.websocket) {
|
|
return;
|
|
}
|
|
|
|
if (this.useProxy) {
|
|
// Skip WebSocket connection when using proxy mode
|
|
console.log('WebSocket disabled in proxy mode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const wsUrl = this.serviceUrl.replace('http', 'ws') + '/ws';
|
|
this.websocket = new WebSocket(wsUrl);
|
|
|
|
this.websocket.onopen = () => {
|
|
console.log('Connected to remote WebGPU service WebSocket');
|
|
this.emit('connected');
|
|
};
|
|
|
|
this.websocket.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
this.emit('message', data);
|
|
|
|
// Handle specific message types
|
|
if (data.type === 'clustering_complete') {
|
|
this.emit('clusteringComplete', data.data);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to parse WebSocket message:', error);
|
|
}
|
|
};
|
|
|
|
this.websocket.onclose = () => {
|
|
console.log('Disconnected from remote WebGPU service WebSocket');
|
|
this.websocket = null;
|
|
this.emit('disconnected');
|
|
};
|
|
|
|
this.websocket.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
this.emit('error', error);
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to connect WebSocket:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect WebSocket
|
|
*/
|
|
disconnectWebSocket(): void {
|
|
if (this.websocket) {
|
|
this.websocket.close();
|
|
this.websocket = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add event listener
|
|
*/
|
|
on(event: string, listener: Function): void {
|
|
if (!this.eventListeners.has(event)) {
|
|
this.eventListeners.set(event, []);
|
|
}
|
|
this.eventListeners.get(event)!.push(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove event listener
|
|
*/
|
|
off(event: string, listener: Function): void {
|
|
const listeners = this.eventListeners.get(event);
|
|
if (listeners) {
|
|
const index = listeners.indexOf(listener);
|
|
if (index > -1) {
|
|
listeners.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit event
|
|
*/
|
|
private emit(event: string, ...args: any[]): void {
|
|
const listeners = this.eventListeners.get(event);
|
|
if (listeners) {
|
|
listeners.forEach(listener => {
|
|
try {
|
|
listener(...args);
|
|
} catch (error) {
|
|
console.error(`Event listener error for ${event}:`, error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup resources
|
|
*/
|
|
dispose(): void {
|
|
this.disconnectWebSocket();
|
|
this.eventListeners.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhanced WebGPU Clustering Engine with Remote Fallback
|
|
* Automatically detects WebGPU availability and falls back to remote service
|
|
*/
|
|
export class EnhancedWebGPUClusteringEngine {
|
|
private localEngine: any | null = null; // WebGPUClusteringEngine
|
|
private remoteClient: RemoteWebGPUClusteringClient;
|
|
private useRemote: boolean = false;
|
|
private isInitialized: boolean = false;
|
|
private lastClusteredData: { nodes: any[], links: any[] } | null = null;
|
|
private clusteringOptions: Partial<RemoteClusteringOptions> = {};
|
|
|
|
constructor(
|
|
clusterDimensions: [number, number, number] = [32, 18, 24],
|
|
remoteServiceUrl: string = 'http://localhost:8083'
|
|
) {
|
|
this.remoteClient = new RemoteWebGPUClusteringClient(remoteServiceUrl, false); // Disable proxy mode for WebSocket
|
|
|
|
// Try to import local WebGPU engine
|
|
this.tryInitializeLocal(clusterDimensions);
|
|
}
|
|
|
|
private async tryInitializeLocal(clusterDimensions: [number, number, number]): Promise<void> {
|
|
// For hybrid mode, skip local WebGPU and go directly to remote
|
|
console.log('Skipping local WebGPU initialization, using remote service for hybrid mode');
|
|
await this.initializeRemote();
|
|
}
|
|
|
|
private async initializeRemote(): Promise<void> {
|
|
try {
|
|
console.log('🔄 Checking remote WebGPU service availability...');
|
|
const available = await this.remoteClient.checkAvailability();
|
|
console.log('🎯 Remote service check result:', available);
|
|
|
|
if (available) {
|
|
this.useRemote = true;
|
|
this.isInitialized = true;
|
|
console.log('✅ Enhanced WebGPU engine initialized with remote cuGraph service');
|
|
|
|
// Skip WebSocket connection for hybrid mode - we only need HTTP API calls
|
|
console.log('⚙️ Using HTTP API for cuGraph clustering (no WebSocket needed)');
|
|
} else {
|
|
console.error('❌ Remote cuGraph service not available - falling back to CPU');
|
|
this.useRemote = false;
|
|
this.isInitialized = false;
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize remote cuGraph service:', error);
|
|
this.useRemote = false;
|
|
this.isInitialized = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if clustering is available (local or remote)
|
|
*/
|
|
isAvailable(): boolean {
|
|
return this.isInitialized;
|
|
}
|
|
|
|
/**
|
|
* Check if using remote service
|
|
*/
|
|
isUsingRemote(): boolean {
|
|
return this.useRemote;
|
|
}
|
|
|
|
/**
|
|
* Get service capabilities
|
|
*/
|
|
getCapabilities(): ServiceCapabilities | null {
|
|
if (this.useRemote) {
|
|
return this.remoteClient.getCapabilities();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the last clustered data (for pre-rendering optimization)
|
|
*/
|
|
getClusteredData(): { nodes: any[], links: any[] } | null {
|
|
return this.lastClusteredData;
|
|
}
|
|
|
|
/**
|
|
* Set clustering options for semantic clustering
|
|
*/
|
|
setClusteringOptions(options: Partial<RemoteClusteringOptions>): void {
|
|
this.clusteringOptions = { ...this.clusteringOptions, ...options };
|
|
console.log('🔧 Updated clustering options:', this.clusteringOptions);
|
|
}
|
|
|
|
/**
|
|
* Update node positions and compute clusters
|
|
*/
|
|
async updateNodePositions(nodes: any[], links: any[] = []): Promise<boolean> {
|
|
console.log('🚀 updateNodePositions called with', nodes.length, 'nodes,', links.length, 'links');
|
|
console.log('🔍 Engine state - initialized:', this.isInitialized, 'useRemote:', this.useRemote);
|
|
|
|
if (!this.isInitialized) {
|
|
console.warn('❌ Enhanced WebGPU clustering engine not initialized');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (this.useRemote) {
|
|
console.log('🌐 Using remote cuGraph clustering service');
|
|
|
|
// Use remote clustering with semantic options
|
|
const result = await this.remoteClient.clusterNodes(nodes, links, {
|
|
mode: 'hybrid',
|
|
forceSimulation: true,
|
|
...this.clusteringOptions
|
|
});
|
|
|
|
console.log('📊 cuGraph clustering result:', result);
|
|
|
|
// Store the clustered data for potential pre-rendering optimization
|
|
if (result.clusteredNodes) {
|
|
this.lastClusteredData = {
|
|
nodes: result.clusteredNodes.map(node => ({
|
|
...node,
|
|
cluster_index: node.cluster_index, // Keep original
|
|
clusterIndex: node.cluster_index // Add camelCase for frontend
|
|
})),
|
|
links: links
|
|
};
|
|
}
|
|
|
|
// Update nodes with clustering results
|
|
if (result.clusteredNodes && result.clusteredNodes.length === nodes.length) {
|
|
let clustersFound = new Set();
|
|
result.clusteredNodes.forEach((clusteredNode, i) => {
|
|
if (nodes[i]) {
|
|
nodes[i].clusterIndex = clusteredNode.cluster_index;
|
|
nodes[i].nodeIndex = clusteredNode.node_index;
|
|
clustersFound.add(clusteredNode.cluster_index);
|
|
// Update positions if force simulation was applied
|
|
if (clusteredNode.x !== undefined) nodes[i].x = clusteredNode.x;
|
|
if (clusteredNode.y !== undefined) nodes[i].y = clusteredNode.y;
|
|
if (clusteredNode.z !== undefined) nodes[i].z = clusteredNode.z;
|
|
}
|
|
});
|
|
console.log(`🎯 cuGraph found ${clustersFound.size} clusters:`, Array.from(clustersFound));
|
|
} else {
|
|
console.warn('⚠️ cuGraph result mismatch - expected', nodes.length, 'got', result.clusteredNodes?.length);
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
console.log('💻 Using local WebGPU engine (fallback)');
|
|
// Use local WebGPU engine
|
|
return this.localEngine?.updateNodePositions(nodes) || false;
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to update node positions:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start WebRTC streaming mode (remote only)
|
|
*/
|
|
async startWebRTCStreaming(nodes: any[], links: any[]): Promise<string | null> {
|
|
if (!this.useRemote) {
|
|
console.warn('WebRTC streaming only available with remote service');
|
|
return null;
|
|
}
|
|
|
|
return await this.remoteClient.startWebRTCStreaming(nodes, links);
|
|
}
|
|
|
|
/**
|
|
* Get WebRTC stream frame URL
|
|
*/
|
|
getStreamFrameUrl(sessionId: string): string | null {
|
|
if (!this.useRemote) {
|
|
return null;
|
|
}
|
|
return this.remoteClient.getStreamFrameUrl(sessionId);
|
|
}
|
|
|
|
/**
|
|
* Add event listener for remote events
|
|
*/
|
|
on(event: string, listener: Function): void {
|
|
if (this.useRemote) {
|
|
this.remoteClient.on(event, listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove event listener
|
|
*/
|
|
off(event: string, listener: Function): void {
|
|
if (this.useRemote) {
|
|
this.remoteClient.off(event, listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read clustered data (local only)
|
|
*/
|
|
async readClusteredData(): Promise<any[] | null> {
|
|
if (this.useRemote) {
|
|
console.warn('readClusteredData not available with remote service');
|
|
return null;
|
|
}
|
|
|
|
return this.localEngine?.readClusteredData() || null;
|
|
}
|
|
|
|
/**
|
|
* Dispose of resources
|
|
*/
|
|
dispose(): void {
|
|
if (this.localEngine) {
|
|
this.localEngine.dispose();
|
|
this.localEngine = null;
|
|
}
|
|
|
|
this.remoteClient.dispose();
|
|
this.isInitialized = false;
|
|
this.useRemote = false;
|
|
}
|
|
}
|